LoginSignup
230
135

More than 5 years have passed since last update.

なぜSwiftのプロトコルはジェネリクスをサポートしないのか

Last updated at Posted at 2018-07-17

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> はジェネリクスによって型パラメータを持ちますが、 Sequenceprotocol 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 のサイズはどうなるのでしょうか? animalCatDog のインスタンスを代入すると何が起こるのでしょうか?

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 等の感覚では、プロトコル型変数を使わずにコードを書くのは難しいように思えます。しかし、大抵のケースはプロトコル型変数を使わなくても同じことができます。

たとえば、先程の CatDog を抽象化して処理を記述する場合を考えてみましょう。

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())
}

どちらの場合でも処理の内容は同じです。決定的に違うのは、後者の AAnimal というプロトコル型ではなく、常に具体的な何らかの型を表すということです。

実際にコンパイラは、最適化の過程でジェネリックな関数やメソッドの型パラメータを具体的な型に展開する場合があります。これを 特殊化( 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 には CatDog が混ざった配列を渡すことができます。

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 の発展の方向性として良い方法だと思っているのは、 CodableEquatable のようにコード生成で 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 の引数や戻り値にプロトコル型は使わない ようにします。ここまでの長い前置きは、このルールを設けるためのものです。次のような問題設定です。

  1. Swift ではプロトコル型変数(や引数・戻り値)を使うことにデメリットがある
  2. プロトコル型は本当に必要な場合に限って必要最小限利用すべき
  3. メソッドの引数や戻り値にプロトコル型を使いたい場合、大抵は書き換えられるので「本当に必要」でないことが多い
    • func useAnimal(_ animal: Animal)func useAnimal<A: Animal>(_ animal: A)
  4. たとえプロトコルがジェネリクスをサポートしても、 Java のようなスタイルでプロトコル型を多用するのではない
  5. その上で、標準ライブラリをジェネリクスで書いてみるとどうなるか

IteratorProtocol

それでは、 IteratorProtocol から始めましょう。 IteratorProtocol は標準ライブラリで次のように宣言されています。

// associatedtype を使った場合
protocol IteratorProtocol {
    associatedtype Element
    mutating func next() -> Element?
}

IteratorProtocolassociatedtype として 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

nextmutating func なのは、このイテレータが値型のときに必要だからです。 next が呼び出される度に異なる値(次の要素を)返そうとすると、自分自身の状態を変更しないといけません。

この IteratorProtocol をジェネリクスで書くと次のようになります。

// ジェネリクスを使った場合
protocol IteratorProtocol<Element> {
    mutating func next() -> Element?
}

特に何の問題もありません。むしろ associatedtype よりもすっきりして感じられます。

Sequence

次は、 IteratorProtocol を使う Sequence プロトコルを考えてみましょう。 SequencemakeIterator メソッドを持ち、イテレータを生成して返します。

標準ライブラリでは、 associatedtype を使って次のように宣言されています(話をシンプルにするために少し単純化しています)。

// associatedtype を使った場合
protocol Sequence {
    associatedtype Element
    associatedtype Iterator: IteratorProtocol where Iterator.Element == Element

    func makeIterator() -> Iterator
}

コードがいきなり複雑になりました。 SequenceElement に加えて、 Iterator という associatedtype を持っています。

IteratormakeIterator の戻り値の型として必要になる associatedtype です。ここに IteratorProtocol と書くことはできません。引数や戻り値にプロトコル型を使わないというルールに違反することもありますが、そもそも IteratorProtocolassociatedtype を持っているのでプロトコル型として利用することができません。そのため、わざわざ Element とは別に Iterator という associatedtype を宣言する必要が生じます。

この Iterator は、 Sequence に適合した具象型では具体的な型となります。たとえば、 Array.IteratorIndexingIterator<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

SequenceElementIterator.Element で不整合が起こってはいけないので、 Iterator には where Iterator.Element == Element という条件が付与されています。この条件がないと、 ElementInt だけど IteratorDouble を返すような変な 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 プロトコルを考えます。 Collectionarray[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 }
}

シンプルですね。しかし、プロトコル型の引数・戻り値を禁止するために SequenceElementIterator の二つの型パラメータを持つんでした。 : Sequence<Element> ではなく、 : Sequence<Element, Iterator> でないといけません。そのためには CollectionIterator という型パラメータを持つ必要が生じます。

ジェネリクスを使って、かつ、プロトコル型の引数・戻り値を禁止すると、 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 の持つ associatedtypeElementIterator だけではありません。他に SubSequence という associatedtype も持っています。

SubSequenceSequence の部分シーケンスを表すのに用いられます。たとえば、 Sequenceprefix メソッドは最初の N 個の要素だけを持つ部分シーケンスを返します。

部分シーケンスの型は Sequence と同じじゃないかと思うかもしれませんが、そうとは限りません。たとえば、 ArraySubSequenceArraySlice です。

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 で書いた場合には SubSequenceIterator については特に指定していませんが、ジェネリクスで書く場合にはこれも明示的に記述しなければなりません。

SequenceIteratorSubSequenceIterator は同じ型で良さそうですが、異なる型にしたいケースもあります。実際に、 ArrayIteartorIndexingIterator<Array<Element>> ですが、 ArraySubSequence である ArraySliceIteratorIndexingIterator<ArraySlice<Element>> という別の型です。 SubSequenceSequence と異なる 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 }
}

これぞ型パラメータ地獄です💀特に、 SubSequenceIndices のように再帰的なパラメータがあるとヤバイです。

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 }
}

MutableCollectionarray[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 }
}

subscriptset を書き足すためだけにこれを書くのはやってられません。

このように、 プロトコル型の引数や戻り値を用いないというルールを定めると、ジェネリクスの記法で書くのは非現実的 です。

ジェネリクスでも、 associatedtype のように型パラメータを暗黙的に受け継ぐ言語仕様にするという方法も考えられるかもしれません。しかし、そもそもプロトコル型変数(や引数・戻り値)はできるだけ避けたいものなので、具象型とプロトコル型で同じジェネリクスの記法を採用するのは好ましくないという見方がありました。それでも、ジェネリクス(の型パラメータ)と associatedtype という類似の仕組みを異なる構文で提供するよりは、ジェネリクスという統一的な構文にした方がシンプルではないかという問いについて考えて来ました。

その結果、 Swift の標準ライブラリが行っていることをジェネリクスの記法で表現しようとすると型パラメータ地獄に陥ることがわかりました。それであれば、 そもそも区別しておきたいものなんだから、型パラメータと associatedtype という異なる構文として提供する方が理に適っています。あえて、ジェネリクスの構文を魔改造して、無理やり型パラメータを省略・継承できるようにする理由はありません。

これが、僕が Swift の現状を元に推察した、 Swift のプロトコルがジェネリクスをサポートしない理由です。

プロトコルがジェネリクスをサポートせず、型パラメータの代わりに associatedtype を用いることで、値型中心の Swift にフィットした API をシンプルに記述できるわけです。

まとめ

  • 値型中心の Swift でプロトコル型変数(や引数・戻り値)を多用したコードを書くとオーバーヘッドが生じる
  • Animal がプロトコルだとして) func foo(_ animal: Animal) ではなく func foo<A: Animal>(_ animal: A) と書くことでプロトコル型引数・戻り値を避けられる場合が多い
  • プロトコルでジェネリクスがサポートされていたとしても、プロトコル型引数・戻り値を使わずに標準ライブラリを再現しようとすると型パラメータ地獄になる💀
230
135
17

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
230
135