Key Path Member Lookup
01 何が問題だったのか
SE-0195 で導入された @dynamicMemberLookup は、subscript(dynamicMember:) の引数として 文字列 を受け取り、メンバー名の解決をランタイムに委ねる仕組みでした。Python など、動的言語のオブジェクトを Swift から扱うブリッジには適しますが、メンバー名は型チェッカの目から見ればただの文字列なので、静的な型情報はほとんど残りません。
一方で、Swift にはプロパティへの参照を値として持ち運べる key path があり、WritableKeyPath<Root, Value> のように「どの型のどのプロパティか」という静的な型情報を保持します。この情報を活かすと、たとえば次のような「値を指す箱」としての Lens を定義できます。
struct Lens<T> {
let getter: () -> T
let setter: (T) -> Void
var value: T {
get { getter() }
nonmutating set { setter(newValue) }
}
}
extension Lens {
func project<U>(_ keyPath: WritableKeyPath<T, U>) -> Lens<U> {
return Lens<U>(
getter: { self.value[keyPath: keyPath] },
setter: { self.value[keyPath: keyPath] = $0 }
)
}
}
Lens<Rectangle> から内部の Point を指す Lens<Point> を得たいとき、利用側は project を介して key path リテラルを渡す必要があります。
struct Point { var x, y: Double }
struct Rectangle { var topLeft, bottomRight: Point }
func projections(lens: Lens<Rectangle>) {
let topLeft = lens.project(\.topLeft) // Lens<Point>
let top = lens.project(\.topLeft.y) // Lens<Double>
}
書けはするものの、project(\.topLeft) のように毎回メソッド呼び出しと \. 記法を挟まなければならず、「Lens が指す値のプロパティへ Lens 経由でアクセスする」という自然な書き味にはなっていません。文字列ベースの @dynamicMemberLookup で似た見た目(lens.topLeft)を作ることはできても、その場合は静的な型情報を失ってしまいます。
02 どのように解決されるのか
@dynamicMemberLookup を拡張し、subscript(dynamicMember:) の引数として KeyPath 系の型を取るオーバーロードを認めます。このオーバーロードを持つ型に対して . や [...] でメンバーアクセスすると、コンパイラはそのアクセスを 対応する key path を引数にした subscript 呼び出しへ書き換え ます。文字列を介さないため、戻り値の型は key path の静的な型情報から完全に決まります。
key path 版の書き方
先ほどの Lens は、project メソッドを subscript(dynamicMember:) に置き換えるだけで、ドット記法でそのまま使えるようになります。
@dynamicMemberLookup
struct Lens<T> {
let getter: () -> T
let setter: (T) -> Void
var value: T {
get { getter() }
nonmutating set { setter(newValue) }
}
subscript<U>(dynamicMember keyPath: WritableKeyPath<T, U>) -> Lens<U> {
return Lens<U>(
getter: { self.value[keyPath: keyPath] },
setter: { self.value[keyPath: keyPath] = $0 }
)
}
}
lens: Lens<Rectangle> に対して、
lens.topLeftはlens[dynamicMember: \.topLeft]に書き換えられ、結果はLens<Point>になります。lens.topLeft.yはlens[dynamicMember: \.topLeft][dynamicMember: \.y]のように、1 階層ずつ subscript 呼び出しに展開され、結果はLens<Double>になります。
key path を一気に \.topLeft.y にまとめるのではなく 1 ステップずつ分解されるため、中間の型(ここでは Lens<Point>)に対する別の @dynamicMemberLookup オーバーロードやメンバーが介在しても、各段階で期待通りに解決されます。
ルックアップのルール
key path ベースのメンバールックアップは、従来の文字列ベースと同じ基本的な制約を引き継ぎつつ、次のように振る舞います。
- 同名の実メンバーが優先される:
@dynamicMemberLookupを持つ型自身に同名のメンバー(プロパティやメソッド)があれば、そちらが優先されます。たとえばLens<Rectangle>自身のvalueは、Rectangleの同名メンバーより優先されます。 - 宣言は型本体にのみ書ける:
@dynamicMemberLookupは型の定義側にのみ指定でき、extension から付与することはできません。 - subscript の形: 非可変長の単一引数で、引数ラベルが
dynamicMember、型がKeyPath/WritableKeyPath/ReferenceWritableKeyPathなどの key path 型である必要があります。 - 文字列版との共存: 同じ型に文字列ベースと key path ベースの両方のオーバーロードがあり、どちらでも解決可能な場合は、より多くの型情報を持つ key path 版が優先されます。これにより、普段は静的に型の付く key path 版を使い、key path で表現できないケースでは文字列版にフォールバックする、といった設計も可能です。