383
198

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Swift 5.1 に導入される Opaque Result Type とは何か

Last updated at Posted at 2019-03-19

Swift 5.1 で Opaque Result Type (SE-0244) が導入されます。

Opaque Result Type とは何でしょうか。例を見てみましょう。次のような AnimalCat があるとします。

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 するだけです。そのため、 PairArraymakeIterator の戻り値の型は同じものになります。ここでは、 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
        }
    }
}

これを使って PairmakeIterator を実装すると次のようになります。

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 という型を露出してしまうのは過剰です。可能であれば PairIteratorprivate な型にしておきたいところです。

これが、「内部実装を過剰に公開している」ということです。

なぜプロトコル型にパフォーマンス上のロスがあるのか

次に、↓のロスがなぜ発生するのかについて説明します。

-> Animal では、パフォーマンス上のロスが発生する

Animal はプロトコルなので、この関数はプロトコル型の値を返すことになります。 Swift ではプロトコル型は Existential Type とも呼ばれます2。 Existential Type には実行時のオーバーヘッドがあり、 Swift の標準ライブラリでは Existential Type はほとんど使われていません( AnySequence などの Type Erasure や Any を除く)。

Existential Type は一見して感じられるよりも多くのことを裏で行っています。たとえば、次のようなコードを考えてみましょう。

var animal: Animal = Cat()
animal = Dog()

animalAnimal 型の変数なので、 CatDog の両方が代入できても何もおかしなことはないように感じられるかもしれません。しかし、 CatDog の実装が次のようになっているとどうでしょうか。

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() で呼ばれるメソッドの実体をコンパイル時に確定させることができます( CatDog がクラスで final でない場合はその限りではありませんが、ここでは CatDog は値型であると仮定します)。 Existential Container を開いて間接的にメソッドを呼び出すようなオーバーヘッドは存在しません。しかし、現実には Animal に適合した型は無限に存在し得ますし、上記のような似たコードの繰り返しは望ましくありません。

ジェネリクスはこのようなオーバーロードをまとめて書くものだと考えるとわかりやすいです4

func useAnimal<A: Animal>(_ animal: A) { animal.foo() }

Swift では、 Specialization という最適化が行われた場合、↑のジェネリクスで書かれた useAnimal は、個別の型に対して書かれた useAnimal と同等のパフォーマンスを実現することができます。 Specialization が行われると、型パラメータに対して具体的な型を埋めたもの(上記の例では ACatDog を埋めたもの)がコンパイル時に生成されます5useAnimal(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 の実装自体が ACat と仮定してしまっています。戻り値のジェネリクスは 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 を行い、 makeAnimalCat を返すのと同じパフォーマンスを発揮することができるわけです。

上記のコードでは 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 は、その実体が同じものであっても別の型とみなされます。そうでなければ、後から makeAnimal2Dog を返すように内部実装が変更されたときに、 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) {
    /* ... */
}

EquatableSelf 引数を持つメソッドを持つので、そもそも Existential Type である Equatable 型の引数を作ることはできませんが、ここでは仮にできるものとしてみましょう。

しかし、それでも assertEquals の引数の型を Equatable とするだけでは制約が不十分です。例えば、次のようなコードでもコンパイルを通ってしまいます。

assertEquals(42, "XYZ") // コンパイル可

第一引数の型は Int 、第二引数の型は String です。 EquatableSelf 同士の比較をするので、異なる型の間の比較は定義されていません。 assertEquals の二つの引数に異なる型の値を渡すのは明確なミスなので、コンパイルエラーになってほしいところです。しかし、 Existential Type ではそのような制約を記述することができません。

ジェネリクスを使えば assertEquals は次のように記述できます。

// ジェネリクス
func assertEquals<T: Equatable>(_ actual: T, _ expected: T) {
    /* ... */
}

この場合、 actualexpected は同じ型 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 は何か一つの具体的な型である必要があります。 CatDog の両方になることはできません。

このように、 Existential Type とジェネリクスはどちらが優れているというわけではなく、それぞれ異なる特性と表現力を持っており、適切に使い分けることが重要です。

Generalized Existential

Existential Type とジェネリクスは適切に使い分けるべきものだということでした。ここで、 Pair.makeIterator が Existential Type を返すようにしたらどうなるかを考えてみましょう。

ただし、 Swift 5.0 時点では実際に Pair.makeIterator が Existential Type を返すことはできません。 Pair.makeIterator が Existential Type を返すためには次の三つをクリアする必要があります。

  1. associatedtype を持つプロトコル型の変数や引数を作ることは認められていない
  2. プロトコル型はそのプロトコル自体に適合しない10
  3. Pair.makeIteratorElementValue である 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 の制約を伴うプロトコル制約を宣言するための、広く役立つ簡略表現の構文になるかもしれない。

someany

このプロポーザル( 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 or any 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 or some strikes me as the corresponding modifier for specific opaque types.

参考訳: Rust はついに existential を修飾なしの状態にするのは悪い考えであり、それら(訳注: Existential Type )の利用が明示的であることを奨励するために dyn キーワードを追加することを決めた(訳注: Existential Type を Aniaml のように書くのではなく、 dyn Animal のように Existential Type であることを明示的に示すキーワードの付与を必要とする構文を採用した)。以前の existential の(訳注: Swift の)構文についての議論の中で、 Anyany は明示的に existential を導入するための修飾子のキーワードとして明確な有力候補だったように思う。言語的にも、そして、あるスタイルを別のスタイルに対して構文上ひいきしないという公平の精神の元でも(訳注: Existential Type と Opaque Type のどちらかを構文上書きやすくして、そちらを優先的に使うことを奨励しないために)、 Somesome は opaque type のための対応する修飾子だと私に思わせる。

この発言は、現在は Swift では Animal 型を単に Animal と書けるけれども、 any Animal のように明示的に Existential Type であることを示す修飾子を付けないといけないような構文も検討していることをうかがわせます。

// Existential Type
let animal: any Animal = Cat()

僕は以前から Swift で Existential Type が濫用されることを懸念していたので12、もし Existential Type が明示的に any というキーワードを必要とするようになるなら、それは素晴らしい変更だと思います。

そして、もし someany の両方が導入されたなら、次のような美しい対応を持つ構文が実現されます。

// ジェネリクス
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 を試すことができるようになりました。

  1. パフォーマンスとメモリ効率の両方の面で、タプルを使った実装の方が優れています。

  2. Swift のプロトコル型がどのように Existential Type であるのかについては @ukitaka さんの "型システムの理論からみるSwiftの存在型(Existential Type)" を御覧ください。

  3. Existential Container についての詳しい説明は本題から外れてしまうのでここでは省略します。わいわいswiftc #5 でもう少し詳しく話したのでそちらを御覧ください。

  4. 厳密にはオーバーロードと異なる挙動をするケースがあります。

  5. デフォルトでは同一モジュール内でしか Specialization は行われませんが、 @inlinable (SE-0193) を用いることでクロスモジュールでも Specialization を働かせることができます。

  6. Reverse generics and opaque result types - Swift Forums

  7. SE-0244: Opaque Result Types - Swift Forums

  8. Opaque argument types - SE-0244 Opaque Result Types

  9. ただし、タプルの値に Opaque Result Type を用いるのは Future Directions の一つであり、このプロポーザルが採用されても Swift 5.1 でできるようにはならないと思われます。

  10. これは、たとえば func useAnimal<A: Animal>(_ animal: A) に対して、 let animal: Animal = Cat() とした animaluseAnimal(animal) のように渡せないことを意味します。プロトコル型がそのプロトコル自体に適合することを self conformance と言いますが、 self conformance が実現されていない理由はこちらで詳しく説明されています。

  11. Generalized existentials - GenericsManifesto

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

383
198
7

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
383
198

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?