Swift のプロトコルは、 Java や C# などのインタフェースのようなものと説明されることが多いですが、いくつかの違いがあります。最も大きな違いの一つが、 Swift のプロトコルはジェネリクスをサポートしていないということです。
Sequence
プロトコルを例にして説明します。 Swift では、 Sequence
プロトコルに適合した型は for
文で要素を取り出すことができます。
// Array は Sequence に適合
let array: Array<Int> = [2, 3, 5]
for element in array {
print(element)
}
struct Array<Element>
はジェネリクスによって型パラメータを持ちますが、 Sequence
は protocol Sequence<Element>
のように宣言されているわけではありません。 Sequence
は次のように宣言されています。
protocol Sequence {
associatedtype Element
associatedtype Iterator: IteratorProtocol where Iterator.Element == Element
func makeIterator() -> Iterator
}
associatedtype
はジェネリクスの型パラメータのようなものに見えるかもしれませんが、 Swift は何のためにわざわざジェネリクスとは別の仕組みを採用しているのでしょうか?
Java や C# のインタフェースはジェネリクスをサポートしています。 Java では、 Swift の Sequence
プロトコルに相当するのは Iterable
インタフェースです。
// Java
interface Iterable<T> {
Iterator<T> iterator();
}
見ての通り Java の Iterable
はジェネリクスを使って宣言されています。 ジェネリクスという一つの仕組みで同じことができるならその方がシンプルです。 Swift がわざわざ associatedtype
を導入したのには何か理由があるはずです。
associatedtype
はジェネリクスに似たことを実現しますが、 Swift を使い始めた人がすぐに気づく決定的な違いがあります。それは、 associatedtype
を持つプロトコル型の変数を作れないことです。
たとえば、 Java では次のようなコードは一般的です。 Iterable
インタフェース型の変数 strings
に、新しく生成した ArrayList
のインスタンスを代入しています。
// Java
Iterable<String> strings = new ArrayList<String>();
しかし、同様のコードは Swift ( 4.1 時点)では書けません。
// Swift
let strings: Sequence<String> = Array<String>() // これはできない⛔️
Java に慣れ親しんでいた僕は、 Swift を使い始めた当初、これに相当とまどいました。なにせ、インタフェース型の変数や引数を使うのは、 Java では息をするように当たり前のことです。一体それなしにどのように抽象化をしろというのでしょう?
ややこしいことに、 Swift でもプロトコル型変数を作れる場合があります。 associatedtype
(と Self
を含む API )を持たないプロトコル型の変数は作ることができます。
protocol Animal {}
struct Cat: Animal {}
let animal: Animal = Cat() // これは OK ✅
そのため、僕が初めて Swift を触ったときは、 associatedtype
を持つプロトコル型変数を作ることができないのは、単に Swift が未成熟で言語機能が足りてないだけかとも思いました(当初の Swift はそう疑われても仕方ない品質でした)。
しかし、 Swift を使う内に、プロトコルがジェネリクスをサポートしないのは Swift によくフィットしていると感じるようになりました。ただ、何度かそのことを説明したいと思ったのですが、なかなかうまく言語化することができませんでした。
1 年半ほど前に @Kuniwak さんの "Swift の型システムが微妙な件" という投稿にコメントしたときも、あまり自分の言いたいことを明確に言えずもやもやしていました。そして、ちょっと前に @amarillons さんに、どうして Swift のプロトコルにはジェネリクスがないのかと質問をされて答える中で、自分の考えていることを大分言語化できてきました。それを整理してまとめたのがこの投稿です。
本当はプロトコルがジェネリクスをサポートしない理由を明言したドキュメントがあれば良かったんですが、僕が探した限りそのようなものは見つまりませんでした。一番関連が深そうなこちらのドキュメントにもざっと目を通してみましたが、ぴたりとその理由について言及されている箇所はなさそうでした。
ですので、本投稿で述べる内容は「なぜSwiftのプロトコルはジェネリクスをサポートしないのか」を、状況証拠を元に僕が推察したことです。
プロトコルを型として使うのは最小限にしたい
先程見たように、 Swift では associatedtype
を持たないプロトコル型の変数を作ることができます。
protocol Animal {}
struct Cat: Animal {}
let animal: Animal = Cat() // OK ✅
しかし、僕はプロトコル型の変数や引数、戻り値を使うことは最小限に留めるべきであると考えています。まずはその理由を説明します。
Swift は値型を中心とした言語です。 Swift の標準ライブラリでも、 KeyPath
などの一部の例外を除き、提供されている型のほぼすべてが値型です。 たとえば、ここに列挙した型は全部値型です。
struct Int { ... }
struct Double { ... }
struct Bool { ... }
struct String { ... }
struct Array<Element> { ... }
struct Dictionary<Key, Value> { ... }
enum Optional<Wrapped> { ... }
では、値型の値をプロトコル型変数に代入するときには何が起こっているのでしょうか?
値型の変数は、その値型のサイズ分だけスタック領域にメモリを確保します。 UInt8
なら 1 バイトの、 Int32
であれば 4 バイトの領域が確保されます。一方で、参照型の場合はインスタンスの実体はヒープ領域に確保され、変数にはその領域へのアドレスが格納されます。アドレスのサイズは環境に依存しますが、 2018 年現在用いられている Swift の環境ではほぼ 8 バイトと考えて問題ないでしょう。
アドレスのサイズは型によらず一定なので、参照型を抽象的に扱うのは簡単です。
class Animal {}
class Cat: Animal {}
class Dog: Animal {}
let cat: Cat = Cat() // 変数 cat は 8 バイト
let dog: Dog = Dog() // 変数 dog は 8 バイト
var animal: Animal // 変数 animal は 8 バイト
animal = cat // cat に格納された 8 バイトのアドレスを animal の 8 バイトの領域にコピー
animal = dog // dog に格納された 8 バイトのアドレスを animal の 8 バイトの領域にコピー
これと似たようなことを値型でやろうとすると、変数 animal
のサイズはどうなるのでしょうか? animal
に Cat
や Dog
のインスタンスを代入すると何が起こるのでしょうか?
protocol Animal {}
struct Cat: Animal { var foo: UInt8 = 2 }
struct Dog: Animal { var bar: Int32 = 3 }
let cat: Cat = Cat() // 変数 cat は 1 バイト
let dog: Dog = Dog() // 変数 dog は 4 バイト
var animal: Animal // 変数 animal は何バイト?🤔
animal = cat // 何が起こる?
animal = dog // 何が起こる?
Swift では、プロトコル型変数に値を代入すると内部的には existential container という入れ物が作られ、そこに格納されます。 existential container のサイズは、その種類や環境によって異なりますが、一般的な環境では先の animal
のサイズは 40 バイトになります。
print(MemoryLayout<Animal>.size) // 40
また、値型の変数経由でメソッドを呼び出すと、実際にどのメソッドが呼び出されるかをコンパイル時に静的に解決できますが、
struct Cat: Animal {
func foo() -> Int { return 2 }
}
let cat = Cat()
// コンパイル時に Cat の foo が呼ばれると確定
print(cat.foo()) // 2
プロトコル型変数経由でメソッドを呼び出そうとすると existential container の中の witness table を引いて実行時に動的に解決しなければいけません。
protocol Animal {
func foo() -> Int
}
struct Cat: Animal {
func foo() -> Int { return 2 }
}
struct Dog: Animal {
func foo() -> Int { return 3 }
}
var animal: Animal = Bool.random() ? Cat() : Dog()
// Cat と Dog のどちらの foo が呼ばれるかを実行時に決定
print(animal.foo()) // 2 or 3
プロトコル型変数を介すとこのようなオーバーヘッドが発生します。 Swift のコードを書くときに Java のようなスタイルでプロトコル型変数や引数・戻り値を多用すると、コード中のありとあらゆる箇所で余計なオーバーヘッドが生じてしまうわけです。
Swift でプロトコル型変数(や引数・戻り値)を使う必要がない場所であえてプロトコル型変数を使うのは、 Java で int
で良いのにあえて Integer
を使って無駄なボクシングを発生させるようなものです。次のコードが無駄であることに異論がある人はいないでしょう。
// Java
static Integer triple(Integer number) {
return number * 3;
}
Integer
ではなく int
を使って次のように書くべきです。
// Java
static int triple(int number) {
return number * 3;
}
これが、僕が Swift においてプロトコル型変数(や引数・戻り値)の利用を本当に必要な場合以外は避けるべきだと考える理由です。
プロトコルを型として使わずにどのように抽象化するか
とは言え、 Java でインタフェース型変数(や引数・戻り値)を使ってコードを抽象化するのは「本当に必要な場合」のように思えます。 Swift でプロトコル型変数(や引数・戻り値)を使うのは「本当に必要な場合」ではないのでしょうか?
Java 等の感覚では、プロトコル型変数を使わずにコードを書くのは難しいように思えます。しかし、大抵のケースはプロトコル型変数を使わなくても同じことができます。
たとえば、先程の Cat
や Dog
を抽象化して処理を記述する場合を考えてみましょう。
protocol Animal {
func foo() -> Int
}
struct Cat: Animal {
func foo() -> Int { return 2 }
}
struct Dog: Animal {
func foo() -> Int { return 3 }
}
Animal
を使う関数 useAnimal
は、プロトコル型引数を使って書くと次のようになります。
func useAnimal(_ animal: Animal) {
print(animal.foo())
}
しかし、この関数はプロトコル型引数を使わなくても次のように書くことができます。
func useAnimal<A: Animal>(_ animal: A) {
print(animal.foo())
}
どちらの場合でも処理の内容は同じです。決定的に違うのは、後者の A
は Animal
というプロトコル型ではなく、常に具体的な何らかの型を表すということです。
実際にコンパイラは、最適化の過程でジェネリックな関数やメソッドの型パラメータを具体的な型に展開する場合があります。これを 特殊化( Specialization ) といいます。前述の func useAnimal<A: Animal>(_ animal: A)
は、特殊化されると次のように書かれているのと等価になります。
func useAnimal(_ animal: Cat) {
print(animal.foo())
}
func useAnimal(_ animal: Dog) {
print(animal.foo())
}
...
これなら、引数 animal
は常に具象型で宣言されているので、 existential container の出番はありません。
実際に、 Swift の標準ライブラリでもほとんどの API は func useAnimal(_ animal: Animal)
ではなく func useAnimal<A: Animal>(_ animal: A)
のスタイルで書かれています。つまり、 Swift では多くのケースにおいて、プロトコル型変数(や引数・戻り値)は「本当に必要」なものではなく避けることができるものなのです。
ジェネリック関数では書けない場合
ただし、いつもこの方法が使えるとは限りません。どのような場合に使えず、そんなときはどうしたらいいかを説明します。
Animal
の配列を受け取り処理を行う useAnimals
という関数を考えてみましょう。この場合、 animals
には Cat
と Dog
が混ざった配列を渡すことができます。
func useAnimals(_ animals: [Animal]) {
animals.forEach { print($0.foo()) }
}
useAnimals([Cat(), Dog()]) // OK ✅
しかし、 Animal
というプロトコル型を使わずにジェネリクスで書こうとすると、 [A]
の A
は一つの型に定まってしまうので Cat
と Dog
を同時に入れることはできません。
func useAnimals<A: Animal>(_ animals: [A]) {
animals.forEach { print($0.foo()) }
}
useAnimals([Cat(), Dog()]) // NG ⛔️
そういうときにはプロトコル型を使うしかありません。しかし、 associatedtype
を持つプロトコルは型として用いることができません。そのような場合は、ワークアラウンドとして type erasure が用いられています。
たとえば、 Sequence
プロトコルには struct AnySequence<Element>
という type erasure が標準ライブラリで提供されています。
let array: Array<Int> = [2, 3, 5]
let set: Set<Int> = [7, 11]
var sequence: AnySequence<Int>
sequence = AnySequence(array)
sequence = AnySequence(set)
このように、 AnySequence<Element>
は Sequence<Element>
の代わりとして振る舞います。
個人的には、この AnySequence
のような type erasure を用いる方式はなかなか良いバランスなのではないかと思っています。
associatedtype
を持たないプロトコルならそのまま型として用いることができますが、ちょっと手軽すぎます。見た目上も普通の型と見分けが付きません。
// Animal はプロトコル?具象型?🤔
let animal: Animal = Cat()
プロトコル型を例外的なものとして意識的に扱うためには、また、 AnySequence
などと統一するためにも、あえて次のような書き方をした方がいいんじゃないかとすら思います。
protocol Animal {}
struct Cat: Animal {}
typealias AnyAnimal = Animal
let animal: AnyAnimal = Cat()
実は過去に、 existential を表すのに、 Animal
のようにプロトコルを型として記述する記法ではなく、 Any<Animal>
のような記法を採用するチャンスがありました。
SE-0095 の変更によって、 Swift 3 からプロトコル P1
, P2
の両方に適合する型を P1 & P2
と書くことができるようになりました。しかし、当初このプロポーザルは P1 & P2
ではなく Any<P1, P2>
という記法を提案していました。 N 個のプロトコルに対して Any<P1, P2, ...>
と書くなら、一つのプロトコルに対して Any<P1>
と書けるべきです。 Any<P1>
は単に P1
と書くのと同じ意味になってしまうので、当初のプロポーザルの記法が採用されていれば、 P1
と書くのはやめて Any<P1>
に統一しようという方向もあり得たかもしれません。
Any<Animal>
という記法であれば、それが existential であることはひと目でわかります。この記法が採用されなかったことは残念です。
protocol Animal {}
struct Cat: Animal {}
let animal: Any<Animal> = Cat()
さらにおもしろいことに、当初のプロポーザルは associatedtype
を持つプロトコルを existential として扱うための記法にも言及しています。
let array: Array<Int> = [2, 3, 5]
let sequence: Any<Sequence where .Element == Int> = array
Sequence
のように associatedtype
を持つプロトコルでも変数や引数・戻り値の型として扱えるようにするための言語仕様である generalized existential について、 Swift のジェネリクスの今後について述べられたドキュメント "Generics Manifesto" で言及されていますが、 SE-0095 の記法( Any<...>
)を元にしています。 P1 & P2
が採用されてしまった今となっては、 generalized existential にどのような記法を採用するのか悩ましそうです。
let array: Array<Int> = [2, 3, 5]
let sequence: Sequence where .Element == Int = array // 読みにくい
このあたりからも、この前 Discord の swift-developers-japan で話題になっていた ABI 安定化前に generalized existential はやらないという話 からも、僕は generalized existential は当分実現しないんじゃないかと思っています。
generalized existential が実現されるまでは、 AnySequence
のような type erasure を使うしかありません。 Sequence
については AnySequence
を使っていれば問題ないんですが、自作のプロトコルに対して AnyFoo
を用意しようとすると大変です。
type erasure の実装方法については、 @omochimetaru の "Swift の Type Erasure の実装パターンの紹介" にまとめられています。
一度実装してみるとわかりますが、めちゃくちゃ面倒です。これをやらないといけないなんて何か言語仕様おかしいだろという気持ちになります。なので、僕は existential の利用は最小限に留めるべきだとは考えていますが、どうしても必要なときに AnyFoo
を実装する手間をなくす方法として考えるなら generalized existential を導入するのもありかなと思います。
ただ、実際に AnyFoo
を実装したくなることは稀なので、必要なときに AnyFoo
を実装するという現状( Swift 4.1 時点)でも十分ではないかとも思います。 generalized existential で言語仕様を複雑にすることと、必要になったときに都度 AnyFoo
を実装する手間を天秤にかけると、後者を採るのもありではないでしょうか。
僕が Swift の発展の方向性として良い方法だと思っているのは、 Codable
や Equatable
のようにコード生成で AnyFoo
を自動生成できるようにすることです。その場合、 generalized existential で言語仕様が複雑になることもなく、現状を踏襲したまま手間だけを軽減できます。
いずれにせよ、現状( Swift 4.1 )では必要に応じて AnyFoo
のような type erasure を実装するしかありません。しかし、普通はそんな事態には滅多に遭遇しないでしょう。
Swift のプロトコルがジェネリクスをサポートしない理由
前置きの説明が長くなりましたがここからが本題です。
ここまで、プロトコル型変数(や引数・戻り値)はできるだけ避け、本当に必要なときに最小限に留めて使用すべきということを述べました。そして、抽象化のためにプロトコル型が必要になることは少なく、大抵は func useAnimal(_ animal: Animal)
の代わりに func useAnimal<A: Animal>(_ animal: A)
のように書くことで、プロトコル型を回避できることを説明しました。
しかし、それだけでプロトコルがジェネリクスをサポートすべきでない理由にはなりません。
確かに、プロトコルがジェネリクスをサポートして次のようなコードが Swift で氾濫するのは避けたいところです。
let array: Array<Int> = [2, 3, 5]
let sequence: Sequence<Int> = array // これは避けたい
しかし、これは generalized existential が使えるようになったとして、それを乱用すべきでないというのと同じ程度の意味しか持ちません。
let array: Array<Int> = [2, 3, 5]
let sequence: Any<Sequence where .Element == Int> = array // これも避けたい
この二つは記法が異なるだけで本質的には同じことを意味しています。 generalized existential の方が記法が複雑なため、カジュアルに使いづらく乱用を防ぎやすいと考えることはできます。しかし、類似した二つのものに異なる記法を採用するより、一つの記法にまとめた方がシンプルで良いという考えもあり得ます。何か決定的に、ジェネリクスの型パラメータと associatedtype
を区別しておかないといけない理由がないと、プロトコルがジェネリクスをサポートしない理由にはなりません。
この理由を探るために、仮にプロトコルにジェネリクスが許されているとして、 Swift の標準ライブラリをジェネリックなプロトコルで書くと何が起こるかを見てみましょう。
ここが重要ですが、これから Swift の標準ライブラリをジェネリックなプロトコルで書き直すに当たって、 API の引数や戻り値にプロトコル型は使わない ようにします。ここまでの長い前置きは、このルールを設けるためのものです。次のような問題設定です。
- Swift ではプロトコル型変数(や引数・戻り値)を使うことにデメリットがある
- プロトコル型は本当に必要な場合に限って必要最小限利用すべき
- メソッドの引数や戻り値にプロトコル型を使いたい場合、大抵は書き換えられるので「本当に必要」でないことが多い
-
func useAnimal(_ animal: Animal)
→func useAnimal<A: Animal>(_ animal: A)
- たとえプロトコルがジェネリクスをサポートしても、 Java のようなスタイルでプロトコル型を多用するのではない
- その上で、標準ライブラリをジェネリクスで書いてみるとどうなるか
IteratorProtocol
それでは、 IteratorProtocol
から始めましょう。 IteratorProtocol
は標準ライブラリで次のように宣言されています。
// associatedtype を使った場合
protocol IteratorProtocol {
associatedtype Element
mutating func next() -> Element?
}
IteratorProtocol
は associatedtype
として Element
を持ち、次の Element
を返すメソッド next
を持ちます。イテレータはシーケンスから一つずつ値を取り出すためのものなので、とても自然な設計です。
普通は for
文の中で暗黙的に利用されるので直接 IteratorProtocol
を使うことは少ないですが、直接使用すると次のようになります。
let array = [2, 3, 5]
var iterator = array.makeIterator()
iterator.next() // Optional(2)
iterator.next() // Optional(3)
iterator.next() // Optional(5)
iterator.next() // nil
next
が mutating func
なのは、このイテレータが値型のときに必要だからです。 next
が呼び出される度に異なる値(次の要素を)返そうとすると、自分自身の状態を変更しないといけません。
この IteratorProtocol
をジェネリクスで書くと次のようになります。
// ジェネリクスを使った場合
protocol IteratorProtocol<Element> {
mutating func next() -> Element?
}
特に何の問題もありません。むしろ associatedtype
よりもすっきりして感じられます。
Sequence
次は、 IteratorProtocol
を使う Sequence
プロトコルを考えてみましょう。 Sequence
は makeIterator
メソッドを持ち、イテレータを生成して返します。
標準ライブラリでは、 associatedtype
を使って次のように宣言されています(話をシンプルにするために少し単純化しています)。
// associatedtype を使った場合
protocol Sequence {
associatedtype Element
associatedtype Iterator: IteratorProtocol where Iterator.Element == Element
func makeIterator() -> Iterator
}
コードがいきなり複雑になりました。 Sequence
は Element
に加えて、 Iterator
という associatedtype
を持っています。
Iterator
は makeIterator
の戻り値の型として必要になる associatedtype
です。ここに IteratorProtocol
と書くことはできません。引数や戻り値にプロトコル型を使わないというルールに違反することもありますが、そもそも IteratorProtocol
は associatedtype
を持っているのでプロトコル型として利用することができません。そのため、わざわざ Element
とは別に Iterator
という associatedtype
を宣言する必要が生じます。
この Iterator
は、 Sequence
に適合した具象型では具体的な型となります。たとえば、 Array.Iterator
は IndexingIterator<Array<Element>>
です。
let numbers: Array<Int> = [2, 3, 5]
var iterator: IndexingIterator<Array<Int>> = numbers.makeIterator()
iterator.next() // Optional(2)
iterator.next() // Optional(3)
iterator.next() // Optional(5)
iterator.next() // nil
Sequence
の Element
と Iterator.Element
で不整合が起こってはいけないので、 Iterator
には where Iterator.Element == Element
という条件が付与されています。この条件がないと、 Element
は Int
だけど Iterator
は Double
を返すような変な Sequence
が実装できてしまいます。
Java の Iterable
インタフェースはもっとシンプルです。 Swift の Sequence
プロトコルを Java の Iterable
っぽく書くと次のようになります。
// ジェネリクスを使って Java 風に書いた場合
protocol Sequence<Element> {
func makeIterator() -> IteratorProtocol<Element>
}
Sequence
は要素のシーケンスを表すので型パラメータ Element
(要素)を持ち、 makeIterator
でその要素を取り出すための IteratorProtocol<Element>
を返す。とてもシンプルです。
しかし、これはルール違反です。プロトコル型 IteratorProtocol<Element>
を makeIterator
の戻り値の型として使ってしまっています。プロトコル型を戻り値に使わないようにするにはどうすればいいでしょうか?
型パラメータ Iterator
を追加し、次のようになります。コードの内容も分量も、 associatedtype
のときとほぼ同じです。これも、特に問題があるようには思えません。
// ジェネリクスを使った場合
protocol Sequence<Element, Iterator: IteratorProtocol<Element>> {
func makeIterator() -> Iterator
}
Collection
さて、次に Sequence
を継承した Collection
プロトコルを考えます。 Collection
は array[index]
のような形で要素にアクセスするための API を表すプロトコルです。
Collection
は標準ライブラリでは次のように宣言されています(こちらも単純化しています)。
// associatedtype を使った場合
protocol Collection: Sequence {
associatedtype Index
subscript(position: Index) -> Element { get }
}
新たな associatedtype
として Index
が導入されています。 Collection
のインデックスは Int
とは限りません。ありとあらゆるインデックスの型に対応できるように、 associatedtype
になっています。
これをジェネリクスを使って Java 風に書いてみると次のようになります。
// ジェネリクスを使って Java 風に書いた場合
protocol Collection<Element, Index>: Sequence<Element> {
subscript(position: Index) -> Element { get }
}
シンプルですね。しかし、プロトコル型の引数・戻り値を禁止するために Sequence
は Element
と Iterator
の二つの型パラメータを持つんでした。 : Sequence<Element>
ではなく、 : Sequence<Element, Iterator>
でないといけません。そのためには Collection
も Iterator
という型パラメータを持つ必要が生じます。
ジェネリクスを使って、かつ、プロトコル型の引数・戻り値を禁止すると、 Collection
は次のようになります。
// ジェネリクスを使った場合
protocol Collection<
Element,
Iterator: IteratorProtocol<Element>,
Index
>: Sequence<Element, Iterator> {
subscript(position: Index) -> Element { get }
}
ちょっと辛くなってきました。 associatedtype
のときと比べて記述量も多くなっています。一体何が原因でしょうか?
associatedtype
で書いたときの Collection
の宣言には一切 Iterator
が現れません。 Element
も、 subscript
の戻り値として現れるだけで、その宣言はどこにも書かれていません。これは、プロトコルを継承したときに associatedtype
も自動的に受け継がれるからです。しかし、ジェネリクスではすべての型パラメータを明示的に記述する必要があります。
引数や戻り値にプロトコル型を使わないというルールの元では、 Iterator
のように、それらをすべて型パラメータで記述しなければいけません。そのため、プロトコル型を使いたいケースに遭遇する度に型パラメータが増え、大変なことになります。
Sequence
(2)
先程は省略しましたが、 Sequence
の持つ associatedtype
は Element
と Iterator
だけではありません。他に SubSequence
という associatedtype
も持っています。
SubSequence
は Sequence
の部分シーケンスを表すのに用いられます。たとえば、 Sequence
の prefix
メソッドは最初の N 個の要素だけを持つ部分シーケンスを返します。
部分シーケンスの型は Sequence
と同じじゃないかと思うかもしれませんが、そうとは限りません。たとえば、 Array
の SubSequence
は ArraySlice
です。
prefix
の戻り値の型を Sequence<Element>
と書くことができれば Array<Element>
だろうと ArraySlice<Element>
だろうと返すことができます。しかし、プロトコル型の戻り値を禁止するルールの元ではそれはできません。 prefix
の戻り値の型は associatedtype SubSequence
で表すことになります。
// associatedtype を使った場合
protocol Sequence {
associatedtype Element
associatedtype Iterator: IteratorProtocol where Iterator.Element == Element
associatedtype SubSequence: Sequence
where Element == SubSequence.Element, SubSequence.SubSequence == SubSequence
func makeIterator() -> Iterator
func prefix(_ maxLength: Int) -> SubSequence
}
これをジェネリクスで書くと次のようになります。
// ジェネリクスを使った場合
protocol Sequence<
Element,
Iterator: IteratorProtocol<Element>,
SubSequenceIterator: IteratorProtocol<Element>,
SubSequence: Sequence<Element, SubSequenceIterator, SubSequence>
> {
func makeIterator() -> Iterator
func prefix(_ maxLength: Int) -> SubSequence
}
associatedtype
で書いた場合には SubSequence
の Iterator
については特に指定していませんが、ジェネリクスで書く場合にはこれも明示的に記述しなければなりません。
Sequence
の Iterator
と SubSequence
の Iterator
は同じ型で良さそうですが、異なる型にしたいケースもあります。実際に、 Array
の Iteartor
は IndexingIterator<Array<Element>>
ですが、 Array
の SubSequence
である ArraySlice
の Iterator
は IndexingIterator<ArraySlice<Element>>
という別の型です。 SubSequence
に Sequence
と異なる Iterator
を指定できるようにするには、 SubSequenceIterator
という新たな型パラメータを導入する必要があります。
そのため、プロトコル型の引数や戻り値を使わないというルールの元で、ジェネリクスを用いて Sequence
を記述しようとすると、 Sequence
は前述のような四つの型パラメータを持つ複雑なプロトコルになってしまいました。
Collection
(2)
Collection
はさらに悲惨なことになります。
associatedtype
で書く場合には、 SubSequence
はどこにも現れません。 Sequence
から自動的に受け継がれるからです。
// associatedtype を使った場合
protocol Collection: Sequence {
associatedtype Index
subscript(position: Index) -> Element { get }
}
しかし、ジェネリクスを使うと次のようになります。めちゃくちゃ長いです。
// ジェネリクスを使った場合
protocol Collection<
Element,
Iterator: IteratorProtocol<Element>,
SubSequenceIterator: IteratorProtocol<Element>,
SubSequence: Sequence<Element, SubSequenceIterator, SubSequence>,
Index
>: Sequence<
Element,
Iterator,
SubSequenceIterator,
SubSequence
> {
subscript(position: Index) -> Element { get }
}
おっと、 Collection
はもう一つ Indices
という associatedtype
を持つんでした。 indices
プロパティでインデックスのコレクションを返すためです。
// associatedtype を使った場合
protocol Collection: Sequence {
associatedtype Index
associatedtype Indices: Collection = DefaultIndices<Self>
where Indices.Element == Index,
Indices.Index == Index,
Indices.SubSequence == Indices
subscript(position: Index) -> Element { get }
var indices: Indices { get }
}
これをジェネリクスで書いてみましょう。
// ジェネリクスを使った場合
protocol Collection<
Element,
Iterator: IteratorProtocol<Element>,
SubSequenceIterator: IteratorProtocol<Element>,
SubSequence: Sequence<Element, SubSequenceIterator, SubSequence>,
Index,
IndicesIterator: IteratorProtocol<Element>,
Indices: Collection<
Element, // Element
IndicesIterator, // Iterator
IndicesIterator, // SubSequenceIterator
Indices, // SubSequence
IndicesIterator, // IndicesIterator
Indices // Indices
>
>: Sequence<
Element,
Iterator,
SubSequenceIterator,
SubSequence
> {
subscript(position: Index) -> Element { get }
var indices: Indices { get }
}
これぞ型パラメータ地獄です💀特に、 SubSequence
や Indices
のように再帰的なパラメータがあるとヤバイです。
MutableCollection
しかし、さらに地獄なのは Collection
が色々なプロトコルのスーパープロトコルなことです。 Collection
を継承しているプロトコルには次のようなものがあります。
protocol BidirectionalCollection: Collection
where SubSequence: BidirectionalCollection,
Indices: BidirectionalCollection { ... }
protocol LazyCollectionProtocol: Collection, LazySequenceProtocol { ... }
protocol MutableCollection: Collection
where SubSequence: MutableCollection { ... }
protocol RangeReplaceableCollection: Collection
where SubSequence: RangeReplaceableCollection { ... }
たとえば、 MutableCollection
は標準ライブラリでは associatedtype
を使って次のように宣言されています(例によって単純化しています)。
// associatedtype を使った場合
protocol MutableCollection: Collection
where SubSequence: MutableCollection
{
subscript(position: Index) -> Element { get set }
}
MutableCollection
は array[index] = 42
のように状態の変更を受け付けるので、 subscript
の宣言に set
が追加されています。 Collection
との差分だけが書かれていてシンプルです。
これと同じことをジェネリクスで書くと次のようになります。もはや、ちゃんと書けているか自信がありません(少なくとも Index
が宣言前に使われているので、型パラメータの順序は入れ替えないといけないはずです)。
// ジェネリクスを使った場合
protocol MutableCollection<
Element,
Iterator: IteratorProtocol<Element>,
SubSequenceIterator: IteratorProtocol<Element>,
SubSequenceIndicesIterator: IteratorProtocol<Index>,
SubSequenceIndices: Collection<
Element, // Element
SubSequenceIndicesIterator, // Iterator
SubSequenceIndicesIterator, // SubSequenceIterator
SubSequenceIndices, // SubSequence
SubSequenceIndicesIterator, // IndicesIterator
SubSequenceIndices // Indices
>,
SubSequence: MutableCollection<
Element, // Element
SubSequenceIterator, // Iterator
SubSequenceIterator, // SubSequenceIterator
SubSequenceIndicesIterator, // SubSequenceIndicesIterator
SubSequenceIndices, // SubSequenceIndices
SubSequence, // SubSequence
Index, // Index
SubSequenceIndicesIterator, // IndicesIterator
SubSequenceIndices // Indices
>,
Index,
IndicesIterator: IteratorProtocol<Element>,
Indices: Collection<
Element, // Element
IndicesIterator, // Iterator
IndicesIterator, // SubSequenceIterator
Indices, // SubSequence
IndicesIterator, // IndicesIterator
Indices // Indices
>
>: Collection<
Element,
Iterator,
SubSequenceIterator,
SubSequence,
Index,
IndicesIterator,
Indices
> {
subscript(position: Index) -> Element { get set }
}
subscript
の set
を書き足すためだけにこれを書くのはやってられません。
このように、 プロトコル型の引数や戻り値を用いないというルールを定めると、ジェネリクスの記法で書くのは非現実的 です。
ジェネリクスでも、 associatedtype
のように型パラメータを暗黙的に受け継ぐ言語仕様にするという方法も考えられるかもしれません。しかし、そもそもプロトコル型変数(や引数・戻り値)はできるだけ避けたいものなので、具象型とプロトコル型で同じジェネリクスの記法を採用するのは好ましくないという見方がありました。それでも、ジェネリクス(の型パラメータ)と associatedtype
という類似の仕組みを異なる構文で提供するよりは、ジェネリクスという統一的な構文にした方がシンプルではないかという問いについて考えて来ました。
その結果、 Swift の標準ライブラリが行っていることをジェネリクスの記法で表現しようとすると型パラメータ地獄に陥ることがわかりました。それであれば、 そもそも区別しておきたいものなんだから、型パラメータと associatedtype
という異なる構文として提供する方が理に適っています。あえて、ジェネリクスの構文を魔改造して、無理やり型パラメータを省略・継承できるようにする理由はありません。
これが、僕が Swift の現状を元に推察した、 Swift のプロトコルがジェネリクスをサポートしない理由です。
プロトコルがジェネリクスをサポートせず、型パラメータの代わりに associatedtype
を用いることで、値型中心の Swift にフィットした API をシンプルに記述できるわけです。
まとめ
- 値型中心の Swift でプロトコル型変数(や引数・戻り値)を多用したコードを書くとオーバーヘッドが生じる
- (
Animal
がプロトコルだとして)func foo(_ animal: Animal)
ではなくfunc foo<A: Animal>(_ animal: A)
と書くことでプロトコル型引数・戻り値を避けられる場合が多い - プロトコルでジェネリクスがサポートされていたとしても、プロトコル型引数・戻り値を使わずに標準ライブラリを再現しようとすると型パラメータ地獄になる💀