Swift Digest
SE-0042 | Swift Evolution

Flattening the function type of unapplied method references

Proposal
SE-0042
Authors
Joe Groff
Review Manager
Chris Lattner
Status
Rejected

01 何が問題だったのか

Swift では、インスタンスメソッドを Type.method のように「型名から」参照して、まだ self を束縛していない関数値として取り出すことができます。このような「未適用のメソッド参照(unapplied method reference)」は、当時カリー化された関数型、つまり (Self) -> (Args...) -> Ret を持っていました。本提案は、この型を「フラットな関数型」 (Self, Args...) -> Ret に変更することを目指したものでした。

カリー化された型は実用上ほとんど嬉しくない

Swift のカリー化された未適用メソッド参照は、イディオムとうまく噛み合いません。標準ライブラリの reducesort、あるいは Cocoa のブロック API は、いずれもフラットな関数型を引数として受け取る形で設計されています。そのため、+ のような自由関数を reduce にそのまま渡すことはできても、二項メソッドである Set.unionSet.union として渡すことはできませんでした。

func sumOfInts(ints: [Int]) -> Int {
  return ints.reduce(0, combine: +) // OK
}

func unionOfSets<T>(sets: [Set<T>]) -> Set<T> {
  // エラー: combine は (Set<T>, Set<T>) -> Set<T> を期待するが、
  // Set.union は (Set<T>) -> (Set<T>) -> Set<T> を持っている
  return sets.reduce([], combine: Set.union)
}

引数を取らない単項メソッドですら (Self) -> () -> Ret という型になってしまい、map のような高階関数に素直に渡せません。

func sortedArrays<T: Comparable>(arrays: [[T]]) -> [T] {
  // エラー: map は [T] -> [T] を期待するが、
  // Array.sort は ([T]) -> () -> [T] を持っている
  return arrays.map(Array.sort)
}

結局、未適用メソッド参照を使うよりも、その場でクロージャを書いた方が素直という状況でした。

mutating メソッドでは未定義動作を引き起こしていた

カリー化された型は mutating メソッドとは根本的に相性が悪い構造を持っていました。mutating メソッドの selfinout として受け渡されますが、inout 引数が有効な「変更ウィンドウ」は、その引数を受け取る関数呼び出しの範囲に限られます。

そのため f(&x)(y) のような連鎖呼び出しを考えると、x への変更権は最初の呼び出しの間だけで失効してしまい、続く (y) の適用時点ではもう x を変更してはいけません。当時の Swift はこの未適用参照をそのままコンパイルしており、部分適用の時点で x へのポインタをキャプチャしてしまうため、完全適用のタイミングでダングリングポインタを使う未定義動作を生み出していました。

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

この提案は Swift Evolution のレビューで一度は採択されたものの、Swift 3 までに実装が間に合いませんでした。その後も未実装のまま時間が経ち、採択時には許容されていた破壊的変更の期限(Swift 3 リリース)を過ぎたことで、最終的に Rejected(却下) となりました。したがって、現在の Swift に本提案の変更は取り込まれていません。

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

採択されていれば、型名やメタタイプを通じて名前解決されたインスタンスメソッドの参照は、self を先頭に置いたフラットな関数値を返すようになる予定でした。

struct Type {
  var x: Int
  func instanceMethod(y y: Int) -> Int {
    return x + y
  }
}

// 現在(提案以前): (Type) -> (y: Int) -> Int
// 提案後:          (Type, y: Int) -> Int
let f = Type.instanceMethod
f(Type(x: 1), y: 2) // => 3

mutating メソッドの場合は、先頭の self パラメータが inout として表現されることになっていました。フラット化によって self と引数が1回の呼び出しの中で揃って渡されるため、inout の変更ウィンドウがずれるという問題も自然に解消します。

struct Type {
  func instanceMethod(x: Int) -> Float { ... }
  mutating func mutatingMethod(x: String) -> Double { ... }
}

// 提案後の型
Type.instanceMethod // : (Type, Int) -> Float
Type.mutatingMethod // : (inout Type, String) -> Double

これにより Set.unionArray.sort のようなメソッドを、reducemap に直接渡せるようになる想定でした。

なお、本提案は「インスタンス経由の部分適用」、つまり instance.instanceMethod の挙動は変更しない、としていました。非 mutating メソッドを self に束縛したクロージャとして取り出す用途は、従来どおり残す想定だったということです。

現在の Swift での状況

未適用メソッド参照のカリー化された型は、本提案が却下された後も取り除かれないまま残っていましたが、最終的に別の提案である SE-0264 によって正式に非推奨化され、Swift 5.x 以降は警告付きで扱われるようになりました。mutating メソッドへの未適用参照についても、採択されていた頃の採択内容に沿って、後続の変更で実装上の破綻を塞ぐ方向で整理されています。

利用者としては、次の点を押さえておけば十分です。

  • Type.method のような未適用参照に頼る書き方は、Swift では推奨されません。必要であればその場でクロージャを書く形にするのが素直です。
// 現在の Swift では、次のような書き方が安全で読みやすい選択肢
let unions = sets.reduce([]) { $0.union($1) }
let sorted = arrays.map { $0.sorted() }
  • mutating メソッドを self に束縛して関数値として取り出したい場合は、instance.method の形の部分適用が引き続き使えます。メソッドをそのまま高階関数に渡したいときには、この形か、上のような明示的なクロージャを利用します。