Swift Digest
SE-0416 | Swift Evolution

Subtyping for keypath literals as functions

Proposal
SE-0416
Authors
Frederick Kellison-Linn
Review Manager
John McCall
Status
Implemented (Swift 6.0)

01 何が問題だったのか

SE-0249 により、keypathリテラルを関数として扱う変換が導入され、\.count のようなリテラルを (Root) -> Value 型の関数が期待される位置にそのまま書けるようになりました。

let strings = ["Hello", "world", "!"]
let counts = strings.map(\.count) // [5, 5, 1]

しかし、この変換は 引数型と戻り値型が完全に一致する場合にしか働かない という制限がありました。通常の関数型同士であれば、戻り値型について共変(covariant)、引数型について反変(contravariant)な変換が認められているのに、keypathリテラルはその恩恵を受けられません。結果として、次のような直感に反する挙動が生じていました。

struct S {
  var x: Int
}

// これらはすべてOK
let f1: (S) -> Int = \.x
let f2: (S) -> Int? = f1
let f3: (S) -> Int? = { $0.x }
let f4: (S) -> Int? = { kp in { root in root[keyPath: kp] } }(\S.x)
let f5: (S) -> Int? = \.x as (S) -> Int

// ところが、直接の変換はエラーになる
let f6: (S) -> Int? = \.x // Error!

(S) -> Int から (S) -> Int? への変換は関数型としては問題なく通るのに、keypathリテラルから書き下すときだけ弾かれてしまう、という非一貫な状態でした。

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

keypathリテラルを関数として扱う際、通常の関数型同士で認められているのと同じ範囲の変換をそのまま適用できるようにします。つまり、root type が Root、value type が Value であるkeypathリテラルは、(Root) -> Value に変換可能なあらゆる関数型へ直接変換できるようになります。

これにより、先ほどエラーになっていた定義もそのまま通ります。

struct S {
  var x: Int
}

let f6: (S) -> Int? = \.x // OK

引数側の反変な変換(サブクラスを受け取る関数型への変換など)も同様に機能します。

class Base {
  var derived: Derived { Derived() }
}
class Derived: Base {}

// (Base) -> Derived のkeypathを (Derived) -> Base として扱える
let g1: (Derived) -> Base = \Base.derived

実際に生成されるコードは従来と変わらず、keypathを一度キャプチャしてから root[keyPath: kp] で読み出す薄いクロージャに展開されます(SE-0249 で示された形)。

// こう書くと…
let f: (User) -> String? = \User.email

// コンパイラはおおむね次のようなコードを生成します
let f: (User) -> String? = { kp in { root in root[keyPath: kp] } }(\User.email)

オーバーロード解決への影響

これまで変換不能だった組み合わせが新たに有効になるため、オーバーロード解決の候補が増える場面があります。たとえば次のようなケースです。

func evil<T, U>(_: (T) -> U) { print("generic") }
func evil(_ x: (String) -> Bool?) { print("concrete") }

evil(\String.isEmpty)

この提案の下でも、keypathの「自然な」関数型は (String) -> Bool であり、(String) -> Bool? にするには追加の変換が必要なので、ジェネリックなオーバーロードの方が選ばれます。新たに候補に入ったオーバーロードは、通常は従来の候補より不利になるため、多くの場合挙動は変わりません。ただし、keypath変換以外の理由で有利になるオーバーロードが新たに候補に入ると結果が変わり得るため、稀なケースでは意図したオーバーロードを明示する必要があるかもしれません。