Swift Digest
SE-0375 | Swift Evolution

Opening existential arguments to optional parameters

Proposal
SE-0375
Authors
Doug Gregor
Review Manager
Xiaodi Wu
Status
Implemented (Swift 5.8)

01 何が問題だったのか

SE-0352 で導入された implicit opening は、any P をジェネリックパラメータ T: P に渡すときにコンパイラが自動で existential を開き、T を underlying type に束縛してくれる機能です。しかし、SE-0352 ではパラメータの型が T? のように Optional になっている場合には existential を開かない、という制限がありました。

protocol P { }

func acceptOptional<T: P>(_ x: T?) { }

func test(p: any P, pOpt: (any P)?) {
  acceptOptional(p)    // SE-0352 では開かれない
  acceptOptional(pOpt) // こちらは nil の可能性があるのでそもそも開けない
}

ここで acceptOptional(pOpt) が開けないのは自然です。pOptnil のときは underlying type が存在せず、T に束縛すべき具象型が決まらないためです。問題は acceptOptional(p) の側で、p は非Optionalの any P なので underlying type は一意に取り出せるにもかかわらず、SE-0352 では「pOpt のケースと挙動がちぐはぐになるのを避けるため」という理由で、あえて開かない仕様になっていました。

この挙動は実用上かなり不便です。「値をそのまま受け取るだけでなく、省略可能な引数として受ける」ために Optional パラメータを取る関数は多く、そのたびに existential を開けずジェネリック化の恩恵が受けられませんでした。回避策として、非Optionalな引数を受けるジェネリックなトランポリン関数を挟む必要があります。

func acceptNonOptionalThunk<T: P>(_ x: T) {
  acceptOptional(x) // トランポリン経由で呼ぶ
}

func test(p: any P) {
  acceptNonOptionalThunk(p) // 回避策
}

単に Optional に包んで呼びたいだけなのに、呼び出しごとに補助関数を用意しなければならないのはボイラープレートが多すぎます。

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

SE-0352 の制限を緩め、非Optionalな existential 引数を Optional なジェネリックパラメータに渡すとき も、existential を開いて T を underlying type に束縛するようにします。

protocol P { }

func openOptional<T: P>(_ value: T?) { }

func testOpenToOptional(p: any P) {
  openOptional(p) // OK: p を開いて T を underlying type に束縛し、Optional に包んで渡す
}

pany P なので underlying type は一意に決まり、開いた値を T?.some として渡せます。トランポリン関数を用意する必要はなくなります。

一方で、Optionalな existential(any P)?)を Optionalなジェネリックパラメータに渡すケースは、従来通り開けません。値が nil のときは underlying type が存在せず、T に束縛する型を決められないためです。

func test(pOpt: (any P)?) {
  openOptional(pOpt) // 依然として開けない
}

この変更は SE-0352 の「開けるケース」を広げるだけで、開ける条件(T の underlying type が一意に決まること)や、開けたときの type erase のルール(戻り値は共変位置で元の existential 相当に erase されるなど)はそのまま引き継がれます。