Swift Digest
SE-0335 | Swift Evolution

Introduce existential any

Proposal
SE-0335
Authors
Holly Borla
Review Manager
Doug Gregor
Status
Implemented (Swift 5.6)

01 何が問題だったのか

Swift には似て非なる2つの「プロトコルの使い方」があります。ひとつはジェネリクスの制約として使う方法、もうひとつは existential type(存在型)として使う方法です。

protocol Animal {}
struct Cat: Animal {}

// (1) ジェネリクスの型パラメータに対する制約
func describe<A: Animal>(_ animal: A) { ... }

// (2) existential type として変数・引数の型に直接書く
func describe(_ animal: Animal) { ... }

(1) は呼び出しごとに具象型 A が決まり、コンパイル時に A ごとの特殊化が可能です。一方 (2) は「Animal に適合する何らかの値」を動的に保持する存在型で、実行時には型消去された入れ物になっています。値は 3 ワードのインラインバッファに収まらなければヒープ割り当てされ、メソッド呼び出しも動的ディスパッチとポインタ間接参照を経由するため、具象型やジェネリクスに比べてコストが大きくつきます。

さらに existential type には意味論上の制限もあります。たとえば associatedtype を持つプロトコルを existential type として使うと、「その値における associatedtype は何か」が呼び出し側で特定できず、手動で適合を書かない限り existential type 自身はそのプロトコルに適合できません。

protocol P {
  associatedtype A
  func test(a: A)
}

func generic<ConcreteP: P>(p: ConcreteP, value: ConcreteP.A) {
  p.test(a: value)
}

func useExistential(p: P) {
  generic(p: p, value: ???) // P.A が何型か決まらない
}

このように existential type はコストも制限も大きい機能ですが、Swift 5.5 までの構文では let animal: Animal = Cat() のように プロトコル名をそのまま型として書くだけ で使えてしまいます。ジェネリクスの制約に書く Animal と、型として書く Animal がまったく同じ見た目であるため、利用者は両者を混同しやすく、本来ジェネリクスで書けば済むところを existential type で書いてしまい、後から限界にぶつかって書き直す、という失敗が頻発していました。見た目が軽いのに実態は重い、という非対称さが progressive disclosure の観点でも問題だったのです。

02 どのように解決されるのか

existential type を使うときは、型の前に any キーワードを明示的に書くようにします。これにより「これは型消去を伴う重い existential type である」ことが構文からも読み取れるようになり、ジェネリクスの制約との区別もはっきりします。

protocol P {}
protocol Q {}
struct S: P, Q {}

let p: any P = S()          // existential type
let pq: any P & Q = S()     // プロトコル合成の existential type

any はプロトコル、プロトコル合成、およびそれらのメタタイプにのみ付けられます。具象型や型パラメータ、関数型に付けるとエラーになります。

struct S {}
let s: any S = S()          // error: 具象型には any は無意味

func generic<T>(t: T) {
  let x: any T = t          // error: 型パラメータには any は無意味
}

AnyAnyObject

AnyAnyObject は名前自体に型消去のニュアンスが含まれており、通常のプロトコルほど有害ではないため、any を付けなくてもそのまま使えます。プロトコル合成の一部として書くことは可能です。

let value: Any = S()
let object: AnyObject = C()

let pObject: any AnyObject & P = C() // 合成では any を書いてよい

メタタイプ

existential metatype(「あるプロトコルに適合する具象型のメタタイプ」)は any P.Type と書きます。一方、existential type any P そのもののメタタイプは (any P).Type と書き、値は (any P).self で取得します。

protocol P {}
struct S: P {}

let existentialMetatype: any P.Type = S.self       // ∃T:P. T.Type
let protocolMetatype: (any P).Type = (any P).self  // (∃T:P. T).Type

実務で使うのはほとんどが前者で、後者は稀です。any P.Type は「P に適合する何らかの TT.Type」と読み替えると理解しやすく、ジェネリックな文脈で型パラメータ T に existential type を代入した際、T.Type が後者(singleton のプロトコルメタタイプ)になる挙動の説明にも役立ちます。

型エイリアスと associated type

プロトコル名そのものへの型エイリアスは、ジェネリクス制約としても existential type としても使えます。一方、any P への型エイリアスは existential type 専用で、ジェネリクス制約には使えません(使用側で改めて any を書く必要はありません)。

protocol P {}
typealias AnotherP = P
typealias AnyP = any P

func generic<T: AnotherP>(value: T) { ... }
func generic<T: AnyP>(value: T) { ... } // error

また、associated type の要件をプロトコル名の型エイリアスだけで満たすことはできず、existential type を使いたい場合は typealias A = any P のように明示する必要があります。

移行と upcoming feature flag

Swift 5.6 で any 構文が導入され、既存の「裸のプロトコル名」もそのまま許容されます。その後のリリースで段階的に警告が追加され、将来の言語モード(Swift 6 以降)では any の省略はエラーになる予定です。現行モードでも upcoming feature flag ExistentialAny(Swift 5.8 で実装)を有効にすれば、先取りして any を必須にできます。移行は機械的なので、マイグレータによる自動変換が可能です。

なお、SE-0309 で Self や associated type を持つプロトコルも existential type として使えるようになった点については、ExistentialAny 導入後に無効化される新コードの量を抑えるため、これらのプロトコルに限っては Swift 5.6 時点から any の明示が必須となっています。

Future Directions

any の導入は、将来的な拡張の土台にもなります。たとえば existential type を extension で拡張して手動で適合を実装できるようにしたり(extension any Equatable: Equatable { ... })、逆に「裸のプロトコル名」を some/ジェネリクスの糖衣構文として再利用する(func append(contentsOf newElements: Sequence<Element>)<S: Sequence> を暗黙に導入する、など)方向も議論されています。これらはあくまで今後の可能性であり、本Proposalで採択されたものではありません。