Swift 5.1 で Opaque Result Type (SE-0244) が導入されます。
Opaque Result Type とは何でしょうか。例を見てみましょう。次のような Animal
と Cat
があるとします。
protocol Animal {
func foo()
}
struct Cat: Animal {
func foo() { print("cat") }
}
このとき、↓の makeAnimal
の戻り値の型である some Animal
のような型が Opaque Result Type です。
func makeAnimal() -> some Animal { // この `some` が新しい
return Cat()
}
let animal = makeAnimal()
animal.foo() // "cat" と表示される
some
という新しい言語機能を導入しなくても、 -> some Animal
の代わりに -> Cat
や -> Animal
とすれば既存の言語機能で同じようなことが実現できます。しかし、
-> Cat
では、内部実装を過剰に公開している-> Animal
では、パフォーマンス上のロスが発生する
という問題がありました。これを解決するために、 内部実装を隠蔽しながらパフォーマンスにも影響しない手段として Opaque Result Type が提案されました。
僕は、 Opaque Result Type は Swift にとって重要な新機能 だと考えています。しかし、なかなかとらえどころのない概念で僕も理解するまでに少し時間がかかりました。そこで、他の言語機能と比較しながらより俯瞰的な視点を導入することで、 Opaque Result Type とは何かをわかりやすく説明したいと思います。
内部実装を過剰に公開するとはどういうことか
まずは↓の意味を、より現実的な例で説明します。
-> Cat
では、内部実装を過剰に公開している
今、二つの値を保持する Pair
型を作りたいとします。この Pair
型は同じ型の値を二つ持ち、 Array
のように for-in
ループで値を取り出せるものを想定しています。 Pair
を使うコードと実行結果の例は次の通りです。
let pair: Pair<Int> = Pair(2, 3)
for value in pair {
print(value)
}
2
3
for-in
ループで値を取り出せるようにするには、 Swift では Sequence
プロトコルに適合している必要があります。そのような Pair
型を実装する簡単な方法は、次のように Array
をラップしてしまうことでしょう。
struct Pair<Value>: Sequence {
private var array: [Value]
init(_ value1: Value, _ value2: Value) {
array = [value1, value2]
}
var values: (Value, Value) { return (array[0], array[1]) }
func makeIterator() -> IndexingIterator<[Value]> {
return array.makeIterator()
}
}
上記のコード中で、 makeIterator
の戻り値の型である IndexingIterator<[Value]>
はあまり見慣れない型だと思います。
Pair.makeIterator
の戻り値の型が IndexingIterator<[Value]>
なのは、 Array.makeIterator
に由来しています。 Pair.makeIterator
は単に array.makeIterator()
の結果を return
するだけです。そのため、 Pair
と Array
の makeIterator
の戻り値の型は同じものになります。ここでは、 array.makeIterator()
の戻り値の型が IndexingIterator<[Value]>
なので、そのまま Pair.makeIterator
の戻り値の型も IndexingIterator<[Value]>
としました。
しかし、これは実装の詳細を不必要に露出してしまっています。
たとえば、↑のコードでは Array
をラップして Pair
を実装していますが、より効率的1な実装とするためにタプルを使って実装することにしてみましょう。
struct Pair<Value>: Sequence {
private(set) var values: (Value, Value)
init(_ value1: Value, _ value2: Value) {
values = (value1, value2)
}
func makeIterator() -> /* ??? */ {
/* ... */
}
}
このとき、 makeIterator
が返すイテレーターを自力で実装する必要があります。
enum PairIterator<Value>: IteratorProtocol {
case first(Value, Value)
case last(Value)
case none
mutating func next() -> Value? {
switch self {
case .first(let first, let last):
self = .last(last)
return first
case .last(let last):
self = .none
return last
case .none:
return nil
}
}
}
これを使って Pair
の makeIterator
を実装すると次のようになります。
struct Pair<Value>: Sequence {
/* ... */
func makeIterator() -> PairIterator<Value> {
return .first(values.0, values.1)
}
}
最初の実装では makeIterator
の戻り値の型は IndexingIterator<[Value]>
でしたが、新しい実装では PairIterator<Value>
になりました。 これは、カプセル化された内部実装を変更したかっただけなのに、公開された API の型が変更されてしまったことを意味します。
加えて、新しい実装では PairIterator
を作りましたが、もしかすると Pair
の実装をさらに変更してやっぱり PairIterator
が不要になるかもしれません。 PairIterator
を返すことは、 将来的に不要になるかもしれない型を公開していることにもなります。 今やりたいことは、 Value
を取り出せる何らかのイテレーターインスタンスを返したいということだけです。そのために PairIterator
という型を露出してしまうのは過剰です。可能であれば PairIterator
は private
な型にしておきたいところです。
これが、「内部実装を過剰に公開している」ということです。
なぜプロトコル型にパフォーマンス上のロスがあるのか
次に、↓のロスがなぜ発生するのかについて説明します。
-> Animal
では、パフォーマンス上のロスが発生する
Animal
はプロトコルなので、この関数はプロトコル型の値を返すことになります。 Swift ではプロトコル型は Existential Type とも呼ばれます2。 Existential Type には実行時のオーバーヘッドがあり、 Swift の標準ライブラリでは Existential Type はほとんど使われていません( AnySequence
などの Type Erasure や Any
を除く)。
Existential Type は一見して感じられるよりも多くのことを裏で行っています。たとえば、次のようなコードを考えてみましょう。
var animal: Animal = Cat()
animal = Dog()
animal
は Animal
型の変数なので、 Cat
と Dog
の両方が代入できても何もおかしなことはないように感じられるかもしれません。しかし、 Cat
と Dog
の実装が次のようになっているとどうでしょうか。
struct Cat: Animal {
var a: UInt8 = 42
}
struct Dog: Animal {
var b: Int64 = -1
}
この場合、 Cat
を変数/定数に格納するには 1 バイト、 Dog
を格納するには 8 バイトの領域を必要とします。
let cat: Cat = Cat()
MemoryLayout.size(ofValue: cat) // 1
let dog: Dog = Dog()
MemoryLayout.size(ofValue: dog) // 8
では、この両方を格納できる Animal
型の変数/定数には何バイトの領域が必要でしょうか?
var animal: Animal = Cat()
MemoryLayout.size(ofValue: animal) // 40
なんと 40 バイトもの領域が必要になりました。これは、 Animal
に適合したどのような型のインスタンスでも格納できるように、 Existential Container という入れ物に包まれているからです3。
Existential Type を使うと、引数に渡すときに Existential Container に包むオーバーヘッドが発生し、メソッドを呼ぶときには Existential Container を開いて間接的にメソッドを呼び出すオーバーヘッドが発生します。
func useAnimal(_ animal: Animal) {
animal.foo() // ここで Existential Container を開くオーバーヘッド発生
}
let cat = Cat()
useAnimal(cat) // ここで Existential Container に包むオーバーヘッド発生
このように、 Existential Type を使うとパフォーマンス上のロスが発生します。
Existential Type のオーバーヘッドをジェネリクスで解決する
Existential Type とジェネリクスの役割は異なりますが、どちらでもできることもあります。たとえば、次のどちらの書き方でも同じような関数を実装することができます。
// Existential Type
func useAnimal(_ animal: Animal) { animal.foo() }
// ジェネリクス
func useAnimal<A: Animal>(_ animal: A) { animal.foo() }
しかし、これらの実行時のパフォーマンスは異なります。
useAnimal
を実装するときに、パフォーマンス的に最良なのは個別の型について書き下すことです。
func useAnimal(_ animal: Cat) { animal.foo() }
func useAnimal(_ animal: Dog) { animal.foo() }
// その他のすべての `Animal` に適合した型についての実装が続く
そうすると、コンパイル時に animal
の型が確定するので、 animal.foo()
で呼ばれるメソッドの実体をコンパイル時に確定させることができます( Cat
や Dog
がクラスで final
でない場合はその限りではありませんが、ここでは Cat
や Dog
は値型であると仮定します)。 Existential Container を開いて間接的にメソッドを呼び出すようなオーバーヘッドは存在しません。しかし、現実には Animal
に適合した型は無限に存在し得ますし、上記のような似たコードの繰り返しは望ましくありません。
ジェネリクスはこのようなオーバーロードをまとめて書くものだと考えるとわかりやすいです4。
func useAnimal<A: Animal>(_ animal: A) { animal.foo() }
Swift では、 Specialization という最適化が行われた場合、↑のジェネリクスで書かれた useAnimal
は、個別の型に対して書かれた useAnimal
と同等のパフォーマンスを実現することができます。 Specialization が行われると、型パラメータに対して具体的な型を埋めたもの(上記の例では A
に Cat
や Dog
を埋めたもの)がコンパイル時に生成されます5。 useAnimal(Cat())
はまるで func useAnimal(_ animal: Cat)
が存在するかのように振る舞います。 Specialization が行われた場合、ジェネリクスには Existential Type と違って実行時のオーバーヘッドがありません。
ジェネリクスと Existential Type は共に抽象的な型に対してコードを書くための手段を提供しますが、ジェネリクスには実行時のオーバーヘッドがないという利点があります。一般的には、ジェネリクスと Existential Type でできることは異なり、適切に使い分けられるべきです。しかし、どちらでもできることもあり、そのような場合はジェネリクスが好ましい場合が多いでしょう。
『リバースジェネリクス』
ジェネリクスを使えば Existential Type の問題を解決できるケースがあることを見ました。しかし、それは引数の話でした。
// Existential Type
func useAnimal(_ animal: Animal) { animal.foo() }
// ジェネリクス
func useAnimal<A: Animal>(_ animal: A) { animal.foo() }
Existential Type を返す次のような関数に対して、同じ様にパフォーマンスの問題を解決するにはどうすれば良いでしょうか?
// Existential Type
func makeAnimal() -> Animal {
return Cat()
}
ジェネリクスではうまくいきません。
// ジェネリクス
func makeAnimal<A: Animal>() -> A {
return Cat() // コンパイルエラー
}
ジェネリクスの型パラメータを決めるのはその API の利用者です。にも関わらず↑のコードは makeAnimal
の実装自体が A
を Cat
と仮定してしまっています。戻り値のジェネリクスは Swift でサポートされていますが、上記のコードは利用者ではなく実装者が型を決定しようとしているので型エラーでコンパイルに失敗します。
今やりたいのは、 -> Cat
とした場合と同じパフォーマンスを実現しながら、 -> Animal
とした場合と同じように具体的な型を隠蔽したいということです。そのためには、ジェネリクスとは異なる概念が必要になります。
Swift Forums で、 @orobio さんがそのような概念を『リバースジェネリクス』と名付けて説明しています6。ここでは、 @orobio さんの記法の代わりに、 Swift Core Team の Joe Groff が言及している記法7を採用してこの『リバースジェネリクス』を考えてみます。
ジェネリクスを用いて useAnimal
を引数の型を抽象的に記述するのに対して、『リバースジェネリクス』を用いて makeAnimal
の戻り値の型を抽象的に記述しようとすると次のようになります。
// ジェネリクス
func useAnimal<A: Animal>(_ animal: A) {
animal.foo()
}
// 『リバースジェネリクス』
func makeAnimal() -> <A: Animal> A {
return Cat()
}
これらを比較すると次のような裏返し(双対)の関係にあります。
-
useAnimal
の 利用者 がA
の具体的な型を定め、useAnimal
の 実装者 は抽象的なA
に対してコードを書く。 -
makeAnimal
の 実装者 がA
の具体的な型を定め、makeAnimal
の 利用者 は抽象的なA
に対してコードを書く。
makeAnimal
を使うコードの例は↓です。
let animal = makeAnimal()
animal.foo()
makeAnimal
で返されるのは実際には Cat
インスタンスですが、 makeAnimal
の利用者にはそれは見えません。あくまで Animal
に適合した何らかの型のインスタンスが返されたものとして扱います。しかし、実際にはそれは Cat
なのでコンパイラが Specialization を行い、 makeAnimal
が Cat
を返すのと同じパフォーマンスを発揮することができるわけです。
上記のコードでは animal
の型を明記していませんが、型推論を使わずに明示的に記述しようとするとどうなるでしょうか。そのような記法は存在しませんが、概念的には次のように書けます。
let animal: makeAnimal.A = makeAnimal()
animal.foo()
makeAnimal.A
とは、 makeAnimal
が返す Animal
に適合した A
型という意味です。 makeAnimal.A
は実際には Cat
ですが、そのことは利用者には隠蔽されているので次のようなコードは型エラーとなります。
let animal = makeAnimal()
let cat: Cat = animal // コンパイルエラー
これは、↓ができないのと似ています。
func useAnimal<A: Animal>(_ animal: A) {
let cat: Cat = animal // コンパイルエラー
cat.foo()
}
また、 makeAnimal.A
を通して( Animal
には宣言されてない) Cat
独自のプロパティやメソッドを呼び出すこともできません。
このように、 ジェネリクスの対となる『リバースジェネリクス』という概念を導入すれば、具体的な型を隠蔽したまま実行時のオーバーヘッドを支払わずに済ませられます。 -> Cat
と -> Animal
のいいとこ取りができるわけです。
シンタックスシュガーとしての Opaque Type
とらえどころのない Opaque Result Type ですが、『リバースジェネリクス』という概念を導入すると、 Opaque Result Type は『リバースジェネリクス』のシンタックスシュガーである とシンプルに考えることができます。 つまり、次の二つが等価だということです。
// 『リバースジェネリクス』
func makeAnimal() -> <A: Animal> A {
return Cat()
}
// Opaque Result Type
func makeAnimal() -> some Animal {
return Cat()
}
同様に、ジェネリック引数のシンタックスシュガーとして、 Opaque Argument Type も提案されています。今回のプロポーザル( SE-0244 )が Accept されてもそこで実装される範囲には含まれていませんが、プロポーザルの Future Directions で言及されています8。
// ジェネリクス
func useAnimal<A: Animal>(_ animal: A) {
animal.foo()
}
// Opaque Argument Type
func useAnimal(_ animal: some Animal) {
animal.foo()
}
Opaque Argument Type と Opaque Result Type をジェネリクスと『リバースジェネリクス』のシンタックスシュガーと考えると、とらえどころのない概念を自然なものとして受け入れることができるのではないでしょうか。
Opaque Result Type の挙動
Opaque Result Type の挙動についてはわかりづらい点もあるので、いくつか例を挙げてみます。
func makeAnimal() -> some Animal {
return Cat()
}
var animal1 = makeAnimal()
let animal2 = makeAnimal()
animal1 = animal2 // OK
↑は当然 OK です。しかし、
func makeAnimal1() -> some Animal {
return Cat()
}
func makeAnimal2() -> some Animal {
return Cat()
}
var animal1 = makeAnimal1()
let animal2 = makeAnimal2()
animal1 = animal2 // コンパイルエラー
↑は、同じ some Animal
同士で、その実体はどちらも Cat
なのにコンパイルエラーになりました。異なる API から返された Opaque Result Type は、その実体が同じものであっても別の型とみなされます。そうでなければ、後から makeAnimal2
が Dog
を返すように内部実装が変更されたときに、 API の型は変わらないのに型チェックの挙動が変わってしまうことになります。
func makeAnimal1() -> some Animal {
return Cat()
}
func makeAnimal2() -> some Animal {
return Dog()
}
var animal1 = makeAnimal1()
let animal2 = makeAnimal2()
animal1 = animal2 // これは当然コンパイルエラーにしないといけない
また、次のように、一つの API で複数の some Animal
が書かれた場合でも、それらは別の型とみなされます9。
func makeAnimals() -> (some Animal, some Animal) {
return (Cat(), Cat())
}
var (animal1, animal2) = makeAnimals()
animal1 = animal2 // コンパイルエラー
これも後から return (Cat(), Dog())
に内部実装が変更されても、利用側で型チェックの挙動が変わらないためには必須です。なお、このケースは『リバースジェネリクス』を用いて次のように考えると理解しやすいです。
func makeAnimals() -> <A: Animal, B: Animal> (A, B) {
return (Cat(), Cat())
}
var (animal1, animal2) = makeAnimals()
animal1 = animal2 // `A` と `B` は異なるのでコンパイルエラー
同じ字面の some Animal
同士で代入できないことには違和感を感じる人もいるかもしれませんが、 "some" (ある) Animal
だと考えると、「ある Animal
」と「ある Animal
」が異なるものであることは言語的に自然です。そういう意味で、 some
というキーワードは秀逸だと思います。
(追記 2019-05-02) Opaque Result Type はすでに実装されマージされているので、現時点でも Swift 5.1 のスナップショットをダウンロードして Opaque Result Type を試すことができます。
Pair
を Opaque Result Type で実現する
前述の Pair.makeIterator
の戻り値の型は PairIterator<Value>
という具体的な型になっており、 makeAnimal
の戻り値を -> Cat
で記述するのと同じ状態でした。
struct Pair<Value>: Sequence {
/* ... */
func makeIterator() -> PairIterator<Value> {
return .first(values.0, values.1)
}
}
これを Opaque Result Type で記述するとどうなるでしょうか。
makeIterator
は単に IteratorProtocol
に適合した型の値を返すというだけでなく、その Element
の型が Value
である必要があります。 Opaque Result Type でそのような制約を記述できる記法が必要です。
プロポーザル( SE-0244 )には、 Future Directions としてそのような制約の記述の記法についての言及があります。そこで書かれている記法の一つを使うと、次のように書くことができます。
struct Pair<Value>: Sequence {
/* ... */
func makeIterator() -> some IteratorProtocol<.Element == Value> {
return PairIterator.first(values.0, values.1)
}
}
これは、『リバースジェネリクス』を使って次のように書くのと等価です。
struct Pair<Value>: Sequence {
/* ... */
func makeIterator() -> <I: IteratorProtocol> I where I.Element == Value {
return PairIterator.first(values.0, values.1)
}
}
もしくは、 <.Element == Value>
の記法が where
節の短縮記法としてジェネリクスや『リバースジェネリクス』でも用いることができるなら次のようにも書けるでしょう。
// 『リバースジェネリクス』
struct Pair<Value>: Sequence {
/* ... */
func makeIterator() -> <I: IteratorProtocol<.Element == Value>> I {
return PairIterator.first(values.0, values.1)
}
}
このとき、 makeIterator
が返すイテレータは PairIterator<Value>
インスタンスですが、 makeIterator
はその型を露出しません。後から内部実装が変わって別のイテレータを返すようになっても、利用側のコードを修正する必要はないでしょう。また、 PairIterator
型も外部に公開する必要はなく、 private
にできます。また、 Specialization が行われれば、 makeIterator
の戻り値の型が PairIterator<Value>
のときと同じパフォーマンスを得ることができます。
このように、 Opaque Result Type または『リバースジェネリクス』を用いることによって、具体的な型の隠蔽とパフォーマンスを両立できました。
ジェネリクスと Existential Type でできることの違い
ここからはやや余談となりますが、 Opaque Result Type の位置付けを探るために関連の言語機能を深掘りしてみます。
本投稿では主にパフォーマンスに着目してジェネリクスの Existential Type に対する優位性を説明しました。しかし、ジェネリクスと Existential Type で表現できることには違いがあり、代替するものではなく互いに補完する存在です。ケース・バイ・ケースで適切に使い分けることが大切です。
ここでは、ジェネリクスでしかできないこと、 Existential Type でしかできないことを見てみます。
ジェネリクスでしかできないこと
単体テストでよく使うような assertEquals
関数を実装することを考えてみましょう。
assertEquals(a, 42) // `a` が `42` になっているか検査する
二つの値を比較して等しいかを検査するためには、その値を比較する手段がなければなりません。 Swift では、 Equatable
プロトコルに適合している値は ==
で比較することができます。
この assertEquals
は Existential Type では書けません。無理やり書こうとすると次のようになってしまいます。
// Existential Type
func assertEquals(_ actual: Equatable, _ expected: Equatable) {
/* ... */
}
Equatable
は Self
引数を持つメソッドを持つので、そもそも Existential Type である Equatable
型の引数を作ることはできませんが、ここでは仮にできるものとしてみましょう。
しかし、それでも assertEquals
の引数の型を Equatable
とするだけでは制約が不十分です。例えば、次のようなコードでもコンパイルを通ってしまいます。
assertEquals(42, "XYZ") // コンパイル可
第一引数の型は Int
、第二引数の型は String
です。 Equatable
は Self
同士の比較をするので、異なる型の間の比較は定義されていません。 assertEquals
の二つの引数に異なる型の値を渡すのは明確なミスなので、コンパイルエラーになってほしいところです。しかし、 Existential Type ではそのような制約を記述することができません。
ジェネリクスを使えば assertEquals
は次のように記述できます。
// ジェネリクス
func assertEquals<T: Equatable>(_ actual: T, _ expected: T) {
/* ... */
}
この場合、 actual
と expected
は同じ型 T
であることが強制されているので、 Existential Type では通ってしまった次のコードをコンパイルエラーにすることができます。
assertEquals(42, "XYZ") // コンパイルエラー
Existential Type でしかできないこと
Existential Type にできてジェネリクスにできないこともあります。 Existential Type を使った次のコードは正しく実行することができます。
// Existential Type
func useAnimals(_ animals: [Animal]) {
/* ... */
}
useAnimals([Cat(), Dog()]) // OK
しかし、ジェネリクスではできません。
func useAnimals<A: Animal>(_ animals: [A]) {
/* ... */
}
useAnimals([Cat(), Dog()]) // コンパイルエラー
型パラメータ A
は何か一つの具体的な型である必要があります。 Cat
と Dog
の両方になることはできません。
このように、 Existential Type とジェネリクスはどちらが優れているというわけではなく、それぞれ異なる特性と表現力を持っており、適切に使い分けることが重要です。
Generalized Existential
Existential Type とジェネリクスは適切に使い分けるべきものだということでした。ここで、 Pair.makeIterator
が Existential Type を返すようにしたらどうなるかを考えてみましょう。
ただし、 Swift 5.0 時点では実際に Pair.makeIterator
が Existential Type を返すことはできません。 Pair.makeIterator
が Existential Type を返すためには次の三つをクリアする必要があります。
-
associatedtype
を持つプロトコル型の変数や引数を作ることは認められていない - プロトコル型はそのプロトコル自体に適合しない10
-
Pair.makeIterator
はElement
がValue
であるIteratorProtocol
を返す必要があるので、その制約を記述しなければならない
1 と 2 はサポートされたものと仮定して、 3 の記法を考えます。前述の Opaque Result Type では some IteratorProtocol<.Element == Value>
という記法を用いました。この制約の記法を Existential Type にも採用することにしましょう。そうすると、 Pair.makeIterator
が Existential Type を返す場合のコードを次のように書くことができます。
struct Pair<Value>: Sequence {
/* ... */
func makeIterator() -> IteratorProtocol<.Element == Value> {
return PairIterator.first(values.0, values.1)
}
}
このような、 Existential Type に対して制約を記述するための構文は Generalized Existential として議論されています11。
なお、↑の IteratorProtocol<.Element == Value>
の記法ついては、僕が説明のために勝手に考えたものではなく、プロポーザル( SE-0244 )の中でも次のように言及されており、 Generalized Existential での利用も見越されているようです(ただし、現時点では候補の一つに過ぎません)。
This could be a generally useful shorthand syntax for declaring a protocol constraint with associated type constraints, usable in where clauses, some clauses, and generalized existentials
参考訳: これ(訳注:
<.Element == Value>
のような制約の記法)はwhere
節やsome
節、 generalized existential で用いることができる、 associated type の制約を伴うプロトコル制約を宣言するための、広く役立つ簡略表現の構文になるかもしれない。
some
と any
このプロポーザル( SE-0244 )のレビューのスレッドの中で、 Swift Core Team の Joe Groff がとても興味深いことを言っています。
Rust eventually decided that making existentials undecorated was a bad idea and added the
dyn
keyword to encourage their use to be explicit. In previous discussions of existential syntax,Any
orany
seemed like the clear favorite as the modifier keyword for explicitly introducing an existential. Both linguistically, and in the spirit of fairness of not syntactically biasing one style over the other,Some
orsome
strikes me as the corresponding modifier for specific opaque types.参考訳: Rust はついに existential を修飾なしの状態にするのは悪い考えであり、それら(訳注: Existential Type )の利用が明示的であることを奨励するために
dyn
キーワードを追加することを決めた(訳注: Existential Type をAniaml
のように書くのではなく、dyn Animal
のように Existential Type であることを明示的に示すキーワードの付与を必要とする構文を採用した)。以前の existential の(訳注: Swift の)構文についての議論の中で、Any
やany
は明示的に existential を導入するための修飾子のキーワードとして明確な有力候補だったように思う。言語的にも、そして、あるスタイルを別のスタイルに対して構文上ひいきしないという公平の精神の元でも(訳注: Existential Type と Opaque Type のどちらかを構文上書きやすくして、そちらを優先的に使うことを奨励しないために)、Some
やsome
は opaque type のための対応する修飾子だと私に思わせる。
この発言は、現在は Swift では Animal
型を単に Animal
と書けるけれども、 any Animal
のように明示的に Existential Type であることを示す修飾子を付けないといけないような構文も検討していることをうかがわせます。
// Existential Type
let animal: any Animal = Cat()
僕は以前から Swift で Existential Type が濫用されることを懸念していたので12、もし Existential Type が明示的に any
というキーワードを必要とするようになるなら、それは素晴らしい変更だと思います。
そして、もし some
と any
の両方が導入されたなら、次のような美しい対応を持つ構文が実現されます。
// ジェネリクス
func useAnimal<A: Animal>(_ animal: A) { /* ... */ }
// Opaque Argument Type (↑のシンタックスシュガー)
func useAnimal(_ animal: some Animal) { /* ... */ }
// Existential (Argument) Type
func useAnimal(_ animal: any Animal) { /* ... */ }
// 『リバースジェネリクス』
func makeAnimal() -> <A: Animal> A { /* ... */ }
// Opaque Result Type (↑のシンタックスシュガー)
func makeAnimal() -> some Animal { /* ... */ }
// Existential (Argument) Type
func makeAnimal() -> any Animal { /* ... */ }
// 制約付きの Opaque Type
let sequence: some Sequence<.Element == Int> = [2, 3, 5]
// Generalized Existential
let sequence: any Sequence<.Element == Int> = [2, 3, 5]
このように考えると Opaque Result Type はよくわからない新しい概念ではなく、 Swift がこれまで欠いていた当然追加されるべきピースの一つにすら見えてきます。
その後の動向
(追記 2019-03-26) レビューの結果が出ました。その結果、 Opaque Result Type 単体としてよりも、 Generalized Existential 等との関係性や Future Directions に示されたような一連の変更を整理して示してから再レビューという流れになったようです。元のプロポーザルの中には Swift 5.1 の記載があったのですが、 Swift 5.1 に間に合うかは怪しくなってきました。個人的には、 Swift はすでにジェネリクスを持っているので( Opaque Result Type と比べて)より汎用的な『リバースジェネリクス』をまずは導入し、その後、シンタックスシュガーとして Opaque Result Type が導入される流れになるのがいいんじゃないかと思ってます。
(追記 2019-04-11) Core Team から Opaque Result Types を含む包括的な方向性を説明するドキュメントが公開されました。これは議論のために概念を説明するもので、方針が決定したわけではありません。また、それを受けて Opaque Result Type の再レビューが開始されました。思ったより早かったので、 Swift 5.1 に間に合うかもしれません。
(追記 2019-04-18) Opaque Result Type の再レビューが終わって即 accept されました。 Swift 5.1 の final branching は終わってしまっているので重要機能として特別に取り込まれない限りは Swift 5.2 か Swift 6.0 で利用可能になると思われます。
(追記 2019-05-02) Opaque Result Type の PR が Swift 5.1 のブランチにマージされ、 Swift 5.1 に導入されることが確定しました。また、スナップショットが公開されたため、ダウンロードして Opaque Result Type を試すことができるようになりました。
-
パフォーマンスとメモリ効率の両方の面で、タプルを使った実装の方が優れています。 ↩
-
Swift のプロトコル型がどのように Existential Type であるのかについては @ukitaka さんの "型システムの理論からみるSwiftの存在型(Existential Type)" を御覧ください。 ↩
-
Existential Container についての詳しい説明は本題から外れてしまうのでここでは省略します。わいわいswiftc #5 でもう少し詳しく話したのでそちらを御覧ください。 ↩
-
厳密にはオーバーロードと異なる挙動をするケースがあります。 ↩
-
デフォルトでは同一モジュール内でしか Specialization は行われませんが、
@inlinable
(SE-0193) を用いることでクロスモジュールでも Specialization を働かせることができます。 ↩ -
ただし、タプルの値に Opaque Result Type を用いるのは Future Directions の一つであり、このプロポーザルが採用されても Swift 5.1 でできるようにはならないと思われます。 ↩
-
これは、たとえば
func useAnimal<A: Animal>(_ animal: A)
に対して、let animal: Animal = Cat()
としたanimal
をuseAnimal(animal)
のように渡せないことを意味します。プロトコル型がそのプロトコル自体に適合することを self conformance と言いますが、 self conformance が実現されていない理由はこちらで詳しく説明されています。 ↩