SwiftのOpaque Type (some)は戻り値の型の抽象化問題を解決するため追加されたものらしい。色々面白かったので整理しておく。

例えとして、とある関数の戻り値を抽象化し、その具体型(concrete type)は公開せずprotocolだけを公開したい場合を考えてみよう。

protocol X { }

struct Foo: X {}
struct Bar: X {}

func returnX() -> X {
  return Foo();
}

Swiftではprotocolを型として使った場合それをexistentialと称す。Existentialは存在型(existential type)であり、その具体型はランタイムに決定される。Existentialの値はExistential Containerというものに型情報と共に包まれる。包む値の大きさが3WORD以上の場合その参照だけを包む。なのでメモリ負荷は定数(現在は5WORD)だが、実行時にExistential Containerに値を包んだり取り出したりする作業が必要になるのでオーバーヘッドが生じる。

func returnX() -> X {
  return Foo();
}

var foo = returnX()
foo = Bar() // OK

func acceptX<T: X>(_ _: T) {}
acceptX(foo) // Error: Value of protocol type 'X' cannot conform to 'X'

Existentialを使うときはtype erasureが起きるので、コンパイル時の型identityが失われる。つまり値の間で型比較ができない。加えて、とあるprotocolのexistentialはそのprotocolを満足しない。

protocol X {
  associatedtype T
}

func returnX() -> X { // Error: ... associated type requirements
  ...
}

Selfやassociatedtypeを使うprotocolの場合、それの型がコンパイル時に不明なためexistentialが作れず、マニュアルでtype erasureを実装しないとならない。AnyCollectionやAnyIteratorなどがmanual type erasureの例である。

引数を抽象化したい場合はgenericsが使える。Genericsは全称型(universal type)であり、コンパイル時にその全ての具体型が確定される。ただSwiftは戻り値に対してのgenerics (reverse generics)はサポートしていない。

Opaque Type (some) はこの問題を解決するために追加された。

func returnX() -> some X {
  return Foo()
}

var foo = returnX()
foo = Foo() // Compile Error
foo = Bar() // Compile Error
foo = returnX() // OK

Opaque Typeを返すと、コンパイル時に実装からその具体型を確定するので型情報が消えない。当然Existential Containerを作る負荷もなくなる。ただ抽象化されているので、使う側は具体型が確認できない。

提案書を見るとOpaque Typeはgenerics改善の一連の修正の最初段階だったそうだ。もっと一般化されたreverse genericsも追加したいらしい。

参考にした資料はこちら。