Swift Digest
SE-0249 | Swift Evolution

Key Path Expressions as Functions

Proposal
SE-0249
Authors
Stephen Celis, Greg Titus
Review Manager
Ben Cohen
Status
Implemented (Swift 5.2)

01 何が問題だったのか

あるルート型からプロパティを取り出すだけの、ごく短いクロージャを書く場面はよくあります。たとえば User 型の配列から email を取り出したり、管理者だけを絞り込んだりするのに、次のようなクロージャを書くことになります。

struct User {
    let email: String
    let isAdmin: Bool
}

users.map { $0.email }
users.filter { $0.isAdmin }

これで十分短いともいえますが、Swift には同じ「ルートから値を取り出す」ことをより直接的に表せる構文として key path リテラル(\User.email など)がすでに存在します。それにもかかわらず、mapfilter のように (Root) -> Value を受け取る API に key path をそのまま渡す方法はなく、ライブラリ側が mapfilter ごとに KeyPath を受け取るオーバーロードを追加したり、^ のようなプレフィックス演算子を自前で定義して key path を関数に変換したりといった回避策で対応するしかありませんでした。高階関数ごとにオーバーロードを用意していくのは現実的ではなく、言語側でのサポートが求められていました。

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

Swift 5.2 で、(Root) -> Value 型の関数が期待される場所に、\Root.value 形式の key path リテラルをそのまま書けるようになります。コンパイラが key path リテラルを暗黙に { $0[keyPath: \Root.value] } 相当のクロージャへ変換してくれるため、高階関数に直接渡せます。

struct User {
    let email: String
    let isAdmin: Bool
}

// 以前は { $0.email } / { $0.isAdmin } と書いていたものを、
users.map(\.email)
users.filter(\.isAdmin)

複数のセグメントを持つ key path や、\.self もそのまま使えます。

users.map(\.email.count)

[1, nil, 3, nil, 5].compactMap(\.self)

key path リテラルに限定される

この変換が効くのは key path リテラル式 だけです。いったん KeyPath 型の値として束縛した変数は、そのままでは (Root) -> Value の引数として渡せません。

let kp = \User.email // KeyPath<User, String>
users.map(kp)        // error: KeyPath を (User) -> String に変換できない

関数型を明示的に要求してやれば、key path リテラルを関数型の値として受け取り直すことはできます。

let f1: (User) -> String = \User.email
users.map(f1)

let f2: (User) -> String = \.email
users.map(f2)

let f3 = \User.email as (User) -> String
users.map(f3)

意味論: クロージャ生成時に副作用が評価される

型検査器は \Root.value の型として優先的に KeyPath<Root, Value>(またはそのサブタイプ)を選びますが、(Root) -> Value が期待されている場所では関数型として扱うこともできます。関数型として扱われた場合、コンパイラは key path をキャプチャしてルート値に適用するのと等価なコードを生成します。概念的には次のような展開です。

// 書いたコード
let f: (User) -> String = \User.email

// コンパイラが生成するコード(意味的に等価)
let f: (User) -> String = { kp in { root in root[keyPath: kp] } }(\User.email)

重要なのは、key path リテラル中の副作用(特に subscript の引数評価)は クロージャが生成される時点で一度だけ 行われ、クロージャが呼び出されるたびに評価し直されるわけではないという点です。

var nextIndex = 0
func makeIndex() -> Int {
    defer { nextIndex += 1 }
    return nextIndex
}

// makeIndex() は 1 回ずつしか呼ばれず、それぞれ \Array<Int>.[0] / \Array<Int>.[1] が出来上がる
let getFirst:  ([Int]) -> Int = \Array<Int>.[makeIndex()]
let getSecond: ([Int]) -> Int = \Array<Int>.[makeIndex()]

getFirst([1, 2, 3])   // 何度呼んでも 1
getSecond([1, 2, 3])  // 何度呼んでも 2

このため、subscript 付きの key path を関数として渡しても、呼び出しごとに添字計算が走ってしまう心配はありません。