Swift Digest
SE-0013 | Swift Evolution

Remove Partial Application of Non-Final Super Methods (Swift 2.2)

Proposal
SE-0013
Authors
Ashley Garland
Review Manager
Doug Gregor
Status
Rejected

01 何が問題だったのか

Swift 2.2 では、クラスのメソッドを super 経由で呼び出すときのディスパッチ方式が変わりました。それまで super.foo() は静的ディスパッチ(呼び先の関数をマングル名で直接呼ぶ方式)でしたが、Swift 2.2 からは(final が付いていない限り)スーパークラスのvtableを介した動的ディスパッチに切り替わります。final が付いていればオーバーライドされないことが保証されるため、従来どおりの静的ディスパッチにフォールバックします。

動的 super ディスパッチと部分適用(currying thunk)の相性が悪い

一方で、Swift 2.x 当時は SE-0002 で削除されることになる「カリー化された関数宣言構文」がまだ残っており、メソッドをすべての引数を与えずに参照する「部分適用」が言語の正式な機能としてサポートされていました。この部分適用を実現するために、コンパイラは内部的にcurry thunkと呼ばれるラッパーを生成しています。

curry thunk の実装は、最終的な呼び出しが「静的な function_ref の apply」か「class_method による動的ディスパッチ」のどちらかである、という前提で組まれていました。ところが、新しい動的 super ディスパッチはどちらの形にも素直に当てはまりません。super 経由の非finalメソッドを正しく部分適用できるようにするには、SILGen・シンボルマングリング・IRGen にまたがる深い手を入れる必要がありました。

Swift 3.0 で消える機能のために深い改修をするのは割に合わない

カリー化構文自体は SE-0002 によって Swift 3.0 で削除されることが決まっていました(func foo()() のような複数引数リストによる宣言が無くなる、という話)。部分適用のためのcurry thunk機構も、その流れで縮小・撤去される見込みでした。

そのような機能のために、Swift 2.2 の段階で SILGen 全体にまたがる大改修を入れるのは、回帰リスクに見合わないと判断されました。かといって super.foo()(末尾の引数リストを与えず部分適用した形)を書けてしまうと、動的 super ディスパッチの実装と整合が取れず壊れたコードを通してしまう恐れがあります。

具体的には、次のようなコードが問題でした。foo()() のようにカリー化された宣言を、派生クラスから super.foo() のように1段だけ適用して部分適用するケースです。

func doFoo(f: () -> ()) {
  f()
}

class Base {
  func foo()() {}
}

class Derived : Base {
  override func foo()() {
    doFoo(super.foo()) // 新しい動的 super ディスパッチではうまく動かない
  }
}

一方で、super.foo(引数リストを1つも与えず、暗黙の self だけを捕まえる形)の部分適用は、partial_apply 命令で素直にクロージャを作れるため問題になりません。また、final が付いたメソッドは静的ディスパッチにフォールバックするため、従来どおり curry thunk でも扱えます。問題となるのは「非final」かつ「super 経由」かつ「引数リストを途中までしか与えない」というケースに限られます。

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

この提案は Rejected(却下) となりました。したがって、Swift に「非finalな super メソッドの部分適用を禁じる」という明示的な制約が入ることはありませんでした。動的 super ディスパッチとカリー化された部分適用の組み合わせに関する実装上の懸念は、SE-0002 によってカリー化構文そのものが Swift 3.0 で削除されたことで自然に解消しています。

提案されていた内容(却下されたもの)

仮に採択されていれば、セマンティック解析の段階で次の条件をすべて満たす呼び出し式をエラーにする、という小さな変更が入る予定でした。

  1. 呼び出しのベースが super である
  2. 参照しているメソッドが final ではない
  3. すべての引数リストを与えきっていない(= 部分適用になっている)

具体例としては、先に挙げた doFoo(super.foo()) のような書き方がエラーになります。一方で、次の2つのケースは引き続き許される想定でした。

final が付いたメソッドに対する部分適用は従来どおり許容されます。final があればオーバーライドされず、新しい動的 super ディスパッチの対象にならないため、元の静的な関数参照にフォールバックできるからです。

func doFoo(f: () -> ()) {
  f()
}

class Base {
  final func foo()() {}
}

class Derived : Base {
  func bar() {
    doFoo(super.foo()) // OK: final なので従来の静的ディスパッチで扱える
  }
}

暗黙の self だけを捕まえる super.foo(末尾の () を付けない形)の部分適用も引き続き許容されます。これは SILGen で追加のthunkを生成する必要がなく、partial_apply 命令だけでクロージャを構築できるため、実装上も安全です。

func doFoo(f: () -> ()) {
  f()
}

class Base {
  func foo() {}
}

class Derived : Base {
  func bar() {
    doFoo(super.foo) // OK: self だけを部分適用している
  }
}

却下された理由

提案自体が「Swift 3.0 のカリー化削除(SE-0002)を Swift 2.2 に部分的に前倒しする」という性格のもので、独立した言語設計上の価値を主張するものではありませんでした。カリー化構文は間もなくまとめて消える予定であり、そのためにわざわざ新しい診断を追加して言語仕様を一時的に複雑にする必要はない、という整理で Rejected となっています。

利用者として知っておくべきこと

  • カリー化された func foo()() のような宣言構文は、SE-0002 により Swift 3.0 で削除されました。現在の Swift で「非finalな super メソッドの部分適用」がそもそも書けないのは、個別の制約が入ったからではなく、カリー化構文自体が無くなったためです。
  • 現在も super.foo(暗黙の self だけを捕まえる形)の部分適用は可能で、クロージャとして取り回せます。スーパークラスのメソッドをそのまま高階関数に渡したい場合などに利用できます。
  • super 経由の呼び出しは、final が付いていなければ動的ディスパッチ、付いていれば静的ディスパッチという挙動自体は、Swift 2.2 以降そのまま現在まで引き継がれています。