Swift Digest
SE-0479 | Swift Evolution

Method and Initializer Key Paths

Proposal
SE-0479
Authors
Amritpan Kaur, Pavel Yaskevich
Review Manager
Becca Royal-Gordon
Status
Returned for revision

01 何が問題だったのか

Swift の key path は、プロパティや subscript への参照を型安全に取り回すための仕組みとして使われてきました。\Calculator.multiplier のように書けば、Calculator インスタンスの multiplier プロパティを後から object[keyPath:] で読み書きできます。

しかし、これまで key path で参照できるメンバーはプロパティと subscript に限られていて、メソッドやイニシャライザは対象外でした。そのため「メソッドへの参照を値として取り回したい」「プロパティと同じように抽象化・汎用化の部品として使いたい」という場面で、key path の恩恵を受けられませんでした。

具体的には、次のような使い方ができませんでした。

  • 未適用のインスタンスメソッドを \Calculator.square のように key path として取り出す
  • 引数適用済みのメソッド呼び出し(\Calculator.square(of: 3))を key path として表現する
  • 型メソッドやイニシャライザを \Calculator.Type.init(multiplier: 5) のようにメタタイプの key path として扱う
  • これらを KeyPath を引数に取るジェネリックな関数や @dynamicMemberLookup を通じて動的に呼び出す

メソッドやイニシャライザもメンバー宣言の一種であり、プロパティや subscript と同じように扱えた方がメンバーアクセス API として一貫します。これができないことで、key path を使った抽象化・再利用の力が、メンバーの種類によって分断されてしまっている状態でした。

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

key path 式がインスタンスメソッド・型メソッド・イニシャライザを参照できるように拡張します。構文は通常のメンバー参照をそのまま key path の形に持ち込んだものです。

struct Calculator {
    func square(of number: Int) -> Int {
        return number * number * multiplier
    }

    func cube(of number: Int) -> Int {
        return number * number * number * multiplier
    }

    init(multiplier: Int) {
        self.multiplier = multiplier
    }

    let multiplier: Int
}

let squareKeyPath = \Calculator.square
let cubeKeyPath = \Calculator.cube

得られた key path は、KeyPath を受け取るジェネリック関数に渡して動的に呼び出せます。

func invoke<T, U>(object: T, keyPath: KeyPath<T, (U) -> U>, param: U) -> U {
    return object[keyPath: keyPath](param)
}

let calc = Calculator(multiplier: 2)
let squareResult = invoke(object: calc, keyPath: squareKeyPath, param: 3)
let cubeResult = invoke(object: calc, keyPath: cubeKeyPath, param: 3)

引数適用の有無

メソッドへの key path は、引数を適用しない形と適用した形の両方で書けます。

let squareWithoutArgs: KeyPath<Calculator, (Int) -> Int> = \Calculator.square
let squareWithArgs: KeyPath<Calculator, Int> = \Calculator.square(of: 3)

引数を適用しない場合、value 型はメソッドの未適用シグネチャ((Int) -> Int)になります。引数を適用した場合は、呼び出し結果の型(Int)になります。

メタタイプメンバー

型メソッド・クラスメソッド・イニシャライザなどメタタイプのメンバーを参照するときは、key path のルート型に .Type を明示的に含めます。

struct Calculator {
    static func add(_ a: Int, _ b: Int) -> Int {
        return a + b
    }
}

let addKeyPath: KeyPath<Calculator.Type, Int> = \Calculator.Type.add(4, 5)
let initializerKeyPath = \Calculator.Type.init(multiplier: 5)

オーバーロードの解決

同じベース名で引数ラベルが異なるメソッドは、引数ラベルを明示することで key path 上でも区別できます。

struct Calculator {
    var subtract: (Int, Int) -> Int { return { $0 + $1 } }
    func subtract(this: Int) -> Int { this + this }
    func subtract(that: Int) -> Int { that + that }
}

let kp1 = \Calculator.subtract            // KeyPath<Calculator, (Int, Int) -> Int>
let kp2 = \Calculator.subtract(this:)     // KeyPath<Calculator, (Int) -> Int>
let kp3 = \Calculator.subtract(that:)     // KeyPath<Calculator, (Int) -> Int>
let kp4 = \Calculator.subtract(that: 1)   // KeyPath<Calculator, Int>

クロージャへの暗黙変換

メソッドへの key path は、クロージャが期待される文脈に暗黙的に変換できます。高階関数に直接渡せるため、map などと組み合わせて簡潔に書けます。

struct Calculator {
    func power(of base: Int, exponent: Int) -> Int {
        return Int(pow(Double(base), Double(exponent)))
    }
}

let calculators = [Calculator(), Calculator()]
let results = calculators.map(\.power(of: 2, exponent: 3))

@dynamicMemberLookup との連携

@dynamicMemberLookup の subscript が KeyPath を受け取る形になっている場合、メソッドへの key path もそのまま解決対象になります。明示的な関数呼び出しを挟まずにメソッド参照を取り出せます。

@dynamicMemberLookup
struct DynamicKeyPathWrapper<Root> {
    var root: Root

    subscript<Member>(dynamicMember keyPath: KeyPath<Root, Member>) -> Member {
        root[keyPath: keyPath]
    }
}

let dynamicCalculator = DynamicKeyPathWrapper(root: Calculator())
let power = dynamicCalculator.power
print(power(10, 2))

コンポーネントの連結

メソッドからさらに別のメンバーへと key path を連結できます。連結結果も従来どおり Hashable / Equatable として扱えます。

let kp5 = \Calculator.subtract(this: 1).signum()
let kp6 = \Calculator.subtract(this: 2).description

サポート対象の範囲

この提案で対応されるのは、nonisolatedconsuming が付いたメソッドまでです。一方で次のものはサポート対象外です。

  • mutating / throws / async が付いたメソッド(そもそも key path の他のコンポーネント種別でも未サポートで、メソッドについても同様に見送られています)
  • noncopyable なルート型や値型
  • Hashable / Equatable でないクロージャ引数をキャプチャするケース

現在のステータス

この提案は現時点で Returned for revision の状態で、実装は experimental feature flag KeyPathWithMethodMembers の下で提供されています。仕様は今後の改訂で変わる可能性があります。

将来への見通し

未サポートの effectful なメソッド(mutating / throws / async など)に対応するには、新しい KeyPath 系の型が必要になります。これは既存の他のコンポーネント種別にも共通する制約でもあり、key path コンポーネント全体にまたがるギャップを一括で埋める提案として将来扱われる可能性があります(本提案のスコープ外で、実現を約束するものではありません)。