Metatype Keypaths
01 何が問題だったのか
Swift の key path 式は、型のプロパティや subscript などをたどって値にアクセスするためのしくみで、\Root.path.to.value の形で書きます。これまでの key path は「インスタンス」のメンバを対象とするものに限定されていて、型そのものに属する static プロパティは対象外でした。
そのため、次のようなコードはコンパイルエラーになっていました。
struct Bee {
static let name = "honeybee"
}
let kp = \Bee.name // error: static member 'name' cannot be used on instance of type 'Bee'
これは、@dynamicMemberLookup と組み合わせて static プロパティを引き当てたい場合や、データベースのカラム定義のように型レベルの情報を key path で参照したい場合に実用上の制約となっていました。回避策として、同じ static プロパティを参照するだけの computed property を用意しておき、そちら越しに key path を作るといった冗長なハックが必要でした。
また、Swift の enum の case への参照は本質的にメタタイプ経由であり、「enum case への key path」という機能を将来検討していくためにも、まずメタタイプを root にできる key path の土台を整える必要がありました。
02 どのように解決されるのか
key path の root 型にメタタイプ(T.Type)を指定できるようにし、static プロパティを指す key path を書けるようにします。これにより、型レベルのメンバも通常のインスタンスメンバと同じように key path で扱えるようになります。
メタタイプを root にする書き方
key path リテラルの最初のコンポーネントが static プロパティを指す場合、root 型には .Type を付けるか、KeyPath<T.Type, ...> のような文脈型を与えます。
struct Bee {
static let name = "honeybee"
}
// key path リテラルで .Type を付ける書き方
let kpWithLiteral = \Bee.Type.name
// 文脈型から root をメタタイプに確定させる書き方
let kpWithContextualType: KeyPath<Bee.Type, String> = \.name
.Type を付けずに \Bee.name と書くと、従来どおり「インスタンス Bee の name を参照している」と解釈され、static member 'name' cannot be used on instance of type 'Bee' というエラーになります。
一方で、static プロパティを参照するコンポーネントが key path の先頭ではない場合は、途中でメタタイプを経由するだけなので .Type を明示する必要はありません。
struct Species {
static let isNative = true
}
struct Wasp {
var species: Species.Type { Species.self }
}
let kpSecondComponentIsStatic = \Wasp.species.isNative
書き込み可能性と key path の種類
static プロパティを指す key path の読み書き可能性は、そのプロパティが let か var かで決まります。イミュータブルな static プロパティは、通常のイミュータブルなインスタンスプロパティと同じく読み取り専用の KeyPath になります。
struct Tip {
static let isIncluded = true
let isVoluntary = false
}
let kpStaticImmutable: KeyPath<Tip.Type, Bool> = \.isIncluded
let kpInstanceImmutable: KeyPath<Tip, Bool> = \.isVoluntary
ミュータブルな static プロパティの場合は、インスタンスのミュータブルプロパティとは異なり、常に ReferenceWritableKeyPath になります。メタタイプは参照型として振る舞うためです。
struct Tip {
static var total = 0
var flatRate = 20
}
let kpStaticMutable: ReferenceWritableKeyPath<Tip.Type, Int> = \.total
let kpInstanceMutable: WritableKeyPath<Tip, Int> = \.flatRate
subscript オーバーロードとの組み合わせ
subscript のオーバーロードが「インスタンスを返すもの」と「メタタイプを返すもの」の両方を持つ場合、文脈型を与えずに key path を書くと、戻り値の型だけからはどちらを指しているのか決められず、型チェックに失敗することがあります。
struct S {
static var count: Int { 42 }
}
struct Test {
subscript(x: Int) -> String { "" }
subscript(y: Int) -> S.Type { S.self }
}
let kpViaSubscript = \Test.[42] // 型が定まらずエラー
この場合は、KeyPath<Test, S.Type> のように value type を明示して subscript を先に確定させ、そこから appending(path:) でメタタイプ key path を連結します。
let kpViaSubscript: KeyPath<Test, S.Type> = \Test.[42]
let kpAppended = kpViaSubscript.appending(path: \.count)
利用上の注意
この機能は back-deploy 可能ですが、static プロパティに対応する新しいシンボル(property descriptor)が必要です。機能未対応のコンパイラでビルドされたモジュールの static プロパティに対して key path を作ろうとすると、次のようなエラーになります。対象モジュールを新しいコンパイラで再ビルドする必要があります。
error: cannot form a keypath to a static property <Property> of type <Type>
note: rebuild <Module> to enable the feature
Future Directions
この提案は、将来検討されている「enum case への key path」への足がかりとしても位置付けられています。enum case の参照はメタタイプ経由になるため、メタタイプを root に持てる key path が先に整うことで、enum case key path の設計が現実的に進めやすくなることが期待されています。ただし構文や実装面の検討事項が別途あるため、本提案の範囲外で、実現が約束されているわけではありません。