Swift Digest
SE-0438 | Swift Evolution

Metatype Keypaths

Proposal
SE-0438
Authors
Amritpan Kaur, Pavel Yaskevich
Review Manager
Joe Groff
Status
Implemented (Swift 6.1)

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 と書くと、従来どおり「インスタンス Beename を参照している」と解釈され、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 の読み書き可能性は、そのプロパティが letvar かで決まります。イミュータブルな 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 の設計が現実的に進めやすくなることが期待されています。ただし構文や実装面の検討事項が別途あるため、本提案の範囲外で、実現が約束されているわけではありません。