第 2 章  Protocol-oriented Programming

Protocol-oriented Programming とは

Value Semantics と違って、 Protocol-oriented Programming という用語は Swift プログラマの間で広く知られています。しかし、筆者の知る限り Protocol-oriented Programming の定義は明確に述べられていません。 WWDC 2015 のセッション “Protocol-Oriented Programming in Swift” の中では、

など数多くの例が取り上げられていますが、何が Protocol-oriented Programming なのかは明確に述べられていません。

それでは、Protocol-oriented Programming とは何なのでしょうか。筆者の解釈は次の通りです。 Protocol-oriented Programming という用語は Object-oriented Programming (オブジェクト指向プログラミング) との対比によって生み出されたものだと、筆者は考えています。 Swift におけるプロトコルは抽象化の道具です。 オブジェクト指向プログラミング における抽象化の側面に目を向けてみると、 オブジェクト指向プログラミング では

を用いてコードを抽象化します。しかし、前章 “Value Semantics” で見てきたように、 Swift は 値型 中心の言語です。 参照型 と異なり、 値型 は原理的に 継承 することができません。そのため、 Swift ではクラスの 継承 ではなくプロトコルが抽象化の主役になります。 Protocol-oriented Programming とは、そのようなプロトコルを用いてコードを抽象化する手法全般を指しているというのが筆者の考えです。

本章では、プロトコルを用いてコードを抽象化する方法と、その結果として必要とされる リバースジェネリクスOpaque Result Type 等について説明します。

プロトコルによる不適切な抽象化

プロトコルを用いた抽象化とはどのようなものでしょうか。 継承 の代わりにプロトコルを使おうと考えると、次のような方法を思い浮かべるかもしれません。

Animal というプロトコルがあるとします。

protocol Animal {
    func foo() -> Int
}

これに適合した二つの structCatDog を考えます。

struct Cat: Animal {
    func foo() -> Int { 2 }
}

struct Dog: Animal {
    func foo() -> Int { 1 }
}

struct値型 ですが、 継承 はできなくてもプロトコルに 適合 することはできます。

今、 CatDogAnimal に適合しているので、 CatDog のインスタンスを Animal 型変数に代入して使うことができます。

let animal: Animal = Bool.random() ? Cat() : Dog() // ✅
print(animal.foo())

オブジェクト指向プログラミング でよく見られるポリモーフィズムです。

しかし、 Swift においてこのようなプロトコルの使い方は必ずしも適切ではありません。なぜ適切でないのか、どのようにプロトコルを使えば良いのかというのが本節のテーマです。

Existential Type と Existential Container

先のコードが不適切な理由を考えるために、コードを少し改変します。 Cat には 1 バイトの、 Dog には 4 バイトの Stored Property を持たせてみましょう。

struct Cat: Animal {
    var value: UInt8 = 2 // 1 バイト
    func foo() -> Int { ... }
}

struct Dog: Animal {
    var value: Int32 = 1 // 4 バイト
    func foo() -> Int { ... }
}

CatDog値型 なので、変数・定数にはそれらのインスタンスが直接格納されます。そのため、 Cat 型変数は 1 バイトの、 Dog 型変数は 4 バイトの領域を必要とします。

let cat: Cat = Cat() // 1 バイト
let dog: Dog = Dog() // 4 バイト

では、 Animal 型変数を作って CatDog のインスタンスを代入するとき、 Animal 型変数は何バイトの領域を必要とするでしょうか。

let animal: Animal = Bool.random() ? cat : dog // 何バイト?🤔

一般的な 64 ビット環境では、 Animal 型変数はなんと 40 バイトもの領域を必要とします。

print(MemoryLayout.size(ofValue: animal)) // 40

1 バイトか 4 バイトの値を格納するだけなら 4 バイトあれば十分なように思えます。なぜ 40 バイトもの領域が必要なのでしょうか。

Animal 型変数に格納できるのは CatDog のインスタンスだけではありません。 Aniaml 型として考えると、 Animal に適合した任意の型のインスタンスを格納できなければなりません。しかし、 Animal に適合した型は、理論上いくらでも大きくできます。たとえば、 1 バイトの Stored Property を 1,000 個持たせれば 1,000 バイトになります。単純に大きな領域を用意するだけでは任意の型のインスタンスを格納することはできません。

そのため、プロトコル型変数にインスタンスを格納する際には、 Existential Container という特殊な入れ物が用いられます。 Existential Container は任意のサイズのインスタンスを格納できる入れ物です。インスタンスは Existential Container に入れられた上で変数に格納されます。上記の例では Existential Container のサイズが 40 バイトなので、 animal のサイズも 40 バイトとなっています。

なお、 Existential Container という名称は、プロトコルを型として用いた場合、その型が Existential Type呼ばれることに由来します。

Existential Container についての詳しい説明は本書の範囲を超えてしまうため、本書ではこれ以上説明しません。 Existential Container については Swift のリポジトリにあるドキュメント “Type Layout”“Existential Container Layout” を御覧下さい。

本書を読み進める上で Existential Container について知っておくべきことは、プロトコル型変数に値を代入すると Existential Container に包まれるということです。そしてそれは、

ということを意味します。

参照型とポリモーフィズム

Existential Container のオーバーヘッドは 値型 とプロトコルに起因するものです。 参照型 であるクラスの 継承 ではそのようなオーバーヘッドは発生しません。

たとえば、先の Animal, Cat, Dog をすべてクラスにしてみましょう。

class Animal {}
class Cat: Animal {}
class Dog: Animal {}

“Value Semantics とは” でも見たように、 参照型 のインスタンスは変数に直接格納されるわけではありません。インスタンスはメモリ上の別の場所に存在し、その場所を指し示すアドレスが変数に格納されます。一般的には、 64 ビット環境においてアドレスは 8 バイト( 64 ビット)で表されます。そのため、 参照型 の変数はどのような型でも 8 バイトの領域しか必要としません。

let cat: Cat = Cat() // 8 バイト
let dog: Dog = Dog() // 8 バイト
let animal: Animal   // 8 バイト
    = Bool.random() ? cat : dog

上記のコードでは、 catdoganimal に格納されているのはすべてインスタンスのアドレスです。 catdoganimal に代入する際に Existential Container に包む必要はありません。

参照型 の場合、 Existential Container のようなオーバーヘッドが存在しないので、 継承ポリモーフィズム による抽象化は理に適った方法だと言えます。

値型に適したポリモーフィズム

それでは、 値型 ではどのようにコードを抽象化すれば良いでしょうか。

抽象化はプログラミングにおける一大テーマです。 Value Semantics のために 値型 中心にした結果、抽象化が妨げられるというのは許容できません。

実は、一口に ポリモーフィズム と言っても様々な種類の ポリモーフィズム があります。ここでは 2 種類の ポリモーフィズム を比較し、 値型 に適した ポリモーフィズム を考えます。

サブタイプポリモーフィズム

オブジェクト指向 の文脈で ポリモーフィズム と言った場合、多くは サブタイプポリモーフィズム を指しています( サブタイピング と呼ばれる方が一般的です)。これまでに本章で見てきた ポリモーフィズム はすべて サブタイプポリモーフィズム です。

プロトコルによって サブタイプポリモーフィズム を実現する場合、プロトコルは 型として 用いられます。次のコードでは、 Animal プロトコルが Animal 型として用いられています。

// サブタイプポリモーフィズム
func useAnimal(_ animal: Animal) {
    print(animal.foo())
}

パラメトリックポリモーフィズム

同様の挙動を示す useAnimal 関数は、 パラメトリックポリモーフィズム を用いても実現できます( パラメータ多相 と呼ばれることも多いです)。

パラメトリックポリモーフィズム を使う場合、プロトコルは 制約として 用いられます。次のコードでは、 Animal プロトコルが 型パラメータ A の制約として用いられています。

// パラメトリックポリモーフィズム
func useAnimal<A: Animal>(_ animal: A) {
    print(animal.foo())
}

実行時に起こること(サブタイプポリモーフィズム)

これらの二つの useAnimal 関数はどちらも同じように使うことができ、実行結果も同じです。

// どちらでも同じように使い、結果も同じ
useAnimal(Cat())
useAnimal(Dog())

しかし、実行時に内部的に行われていることはまったく異なります。

サブタイプポリモーフィズム の場合、 useAnimal に引数を渡すときにインスタンスを Existential Container に包む必要があります。

// サブタイプポリモーフィズム
useAnimal(Cat()) // Existential Container に包む
useAnimal(Dog()) // Existential Container に包む

また、 useAnimal の中で animal.foo() を呼び出す箇所では、 animalCatDog か、または別の型のインスタンスかは実行時までわかりません。 Catfoo を呼び出すか Dogfoo を呼び出すかは実行時に決定されます( 動的ディスパッチ )。

// サブタイプポリモーフィズム
func useAnimal(_ animal: Animal) {
    print(animal.foo()) // 動的ディスパッチ
}

動的ディスパッチ には当然実行時のオーバーヘッドがありますが、 Existential Type を経由してメソッドを呼び出すオーバーヘッドはそれだけではありません。クラスの継承の場合は vtable を介してメソッドを見つけるだけなので大したオーバーヘッドではないですが、 Existential Type を経由してメソッドを呼び出す場合は、一度 Existential Container からインスタンスを取り出すオーバーヘッドも発生します。

実行時に起こること(パラメトリックポリモーフィズム)

パラメトリックポリモーフィズム の場合はどうなるでしょうか。

パラメトリックポリモーフィズム の場合、 useAnimal型パラメータ A を持つジェネリック関数です。ジェネリック関数は、様々な型に対して個別に関数を実装する代わりに、まとめて一つの関数として実装する手段と言えます。

たとえば、オーバーロードを用いて Cat 用の useAnimal 関数と Dog 用の useAnimal 関数を個別に実装することもできます。

// Cat 用の useAnimal
func useAnimal(_ animal: Cat) {
    print(animal.foo())
}

// Dog 用の useAnimal
func useAnimal(_ animal: Dog) {
    print(animal.foo())
}

しかし、これでは類似のコードが重複してしまいます。また、 Animal プロトコルに適合する型は CatDog だけではありません。それらのすべての型に対応しようとするとキリがありません。

そこで、それぞれの型に対して関数をオーバーロードする代わりに、 型パラメータ という概念を導入して一つの関数として実装できるようにしたのがジェネリック関数です。

// ジェネリック関数として実装された useAnimal
func useAnimal<A: Animal>(_ animal: A) { // A が Cat や Dog などを表す
    print(animal.foo())
}

概念的には、ジェネリック関数は 型パラメータCatDog などの具体的な型を当てはめ、それらの関数がオーバーロードされているのと(ほぼ)同じです。上記のような useAnimal 関数を一つ実装すれば、 Cat 用の useAnimalDog 用の useAnimal を個別実装してオーバーロードするのと同じように振る舞います。

さらに、概念上だけでなく実行時の挙動もそれと似たものになります。コンパイラが 特殊化 (最適化の一種)を行った場合、ジェネリック関数としての useAnimal のバイナリに加えて、型パラメータに CatDog などの具体的な型を当てはめた useAnimal のバイナリも生成されます。そして、 useAnimalCat インスタンスを渡す箇所では Cat 用の useAnimal が、 Dog インスタンスを渡す箇所では Dog 用の useAnimal が呼び出されるようにコンパイルされます。

そのため、 サブタイプポリモーフィズム の場合と異なり、 useAnimalCat インスタンスや Dog インスタンスを渡す際に Existential Container に包む必要はありません。 Cat インスタンスは CatuseAnimal に、 Dog インスタンスは DoguseAnimal にそのまま直接渡されます。

// パラメトリックポリモーフィズム
useAnimal(Cat()) // Cat のまま直接渡される
useAnimal(Dog()) // Dog のまま直接渡される

useAnimal 関数の中で animal.foo() を呼び出す箇所についても、 Cat 用の useAnimal の中では animalCat であることがわかっているので、コンパイル時に Catfoo を呼び出せば良いと決定できます( 静的ディスパッチ )。そのため、実行時にメソッドを選択するオーバーヘッドが発生しません。

// パラメトリックポリモーフィズム
func useAnimal<A: Animal>(_ animal: A) {
    print(animal.foo()) // 静的ディスパッチ
}

さらに、 静的ディスパッチ の場合は、コンパイラが最適化によってメソッド呼び出しを インライン展開インライン化 )することがあります。 インライン展開 とは、関数やメソッドの呼び出し箇所に、その関数やメソッドの中身を展開して埋め込むことです。そうすることで、実行時に関数やメソッドを呼び出すオーバーヘッドがゼロになり、パフォーマンスを向上させることができます(ただし、コンパイル後のバイナリのサイズが増大するなどの不利益も存在します。サイズが増大するのは、一つの関数が複数箇所に展開されることになるためです)。

このように、 パラメトリックポリモーフィズム を用いると、実行時のオーバーヘッドを発生させずにコードを抽象化することができます。上記のコードでは、 Cat 用の useAnimalDog 用の useAnimal が一つのジェネリック関数として抽象化されていますが、実行時のオーバーヘッドはありません。 値型 中心の Swift においては、 値型 と組み合わせたときにオーバーヘッドの大きい サブタイピングポリモーフィズム よりも、 値型 であってもオーバーヘッドの発生しない パラメトリックポリモーフィズム の方が適しています。 実際、 Swift の標準ライブラリで用いられている ポリモーフィズム のほとんどが パラメトリックポリモーフィズム です。

ただし、いつでも パラメトリックポリモーフィズム を用いれば良いというわけではありません。 useAnimal 関数は サブタイプポリモーフィズムパラメトリックポリモーフィズム のどちらを用いても実装することができました。そのような場合には、 パラメトリックポリモーフィズム を用いると良いでしょう。しかし、二つの ポリモーフィズム のどちらか片方でしかできないこともあります。 サブタイプポリモーフィズム でしかできないことには サブタイプポリモーフィズム を用いる必要があります(二つの ポリモーフィズム の使い分けについては次節で詳しく説明します)。

制約としてのプロトコル

サビタイプポリモーフィズム よりも パラメトリックポリモーフィズム を優先するということを、プロトコルを中心にして言い替えると次のようになります。

“Protocol-Oriented Programming in Swift” の中では特別そのことは強調されていません。しかし、本節で述べた内容や標準ライブラリにおけるプロトコルの利用例を見る限り、これこそが Swift のプロトコルの使い方ついて最も重要なことだと筆者は考えています。

まとめ

Protocol-oriented Programming という用語は広く知られていますが、筆者の知る限り明確に定義されていません。筆者の解釈では、 Object-oriented Programmingオブジェクト指向プログラミング )との対比となる用語であり、プロトコルを用いてコードを抽象化する手法全般を指していると考えています。

オブジェクト指向プログラミング においてはコードの抽象化のために サブタイプポリモーフィズム が多用されます。しかし、プロトコルと 値型 を使って サブタイプポリモーフィズム を実現しようとすると、 Existential Container に包んだり取り出したりするオーバーヘッドが生じます。 Swift は 値型 を中心とした言語であり、 オブジェクト指向言語 と同じような方法で サブタイプポリモーフィズム を用いるとパフォーマンスに悪影響を与えます。

パラメトリックポリモーフィズム を用いれば、プロトコルと 値型 に対しても、実行時のオーバーヘッドが生じない形でコードを抽象化できます。そのため、 Swift においては サブタイプポリモーフィズム よりも パラメトリックポリモーフィズム を優先的に使用するのが望ましいです。ただし、 サビタイプポリモーフィズムパラメトリックポリモーフィズム はまったく同じことができるわけではありません。どちらかでしか実現できないことに対しては、適切に二つの ポリモーフィズム を使い分ける必要があります。

プロトコルに着目すると、上記の話は、プロトコルを「型として」ではなく「制約として」使用することを優先すると言い替えることができます。それが、 Protocol-oriented Programming において最も重要なことだと筆者は考えています。