- Proposal: SE-0479
- Authors: Amritpan Kaur, Pavel Yaskevich
- Review Manager: Becca Royal-Gordon
- Status: Active Review (April 22 ... May 5, 2025)
- Implementation: swiftlang/swift#78823, swiftlang/swiftsyntax#2950, swiftlang/swiftfoundation#1179
- Experimental Feature Flag:
KeyPathWithMethodMembers
- Review: (pitch)
Swift key paths can be written to properties and subscripts. This proposal extends key path usage to include references to method members, such as instance and type methods, and initializers.
Key paths to method members and their advantages have been explored in several discussions on the Swift forum, specifically to unapplied instance methods and to partially and applied methods. Extending key paths to include reference to methods and initializers and handling them similarly to properties and subscripts will unify instance and type member access for a more consistent API. Key path methods and initializers will also enjoy all of the benefits offered by existing key path component kinds, e.g. simplify code by abstracting away details of how these properties/subscripts/methods are modified/accessed/invoked, reusability via generic functions that accept key paths to methods as parameters, and supporting dynamic invocation while maintaining type safety.
We propose the following usage:
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
}
// Key paths to Calculator methods
let squareKeyPath = \Calculator.square
let cubeKeyPath = \Calculator.cube
These key paths can then be invoked dynamically with a generic function:
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)
Or used to dynamically create a new instance of Calculator:
let initializerKeyPath = \Calculator.Type.init(multiplier: 5)
This proposed feature homogenizes the treatment of member declarations by extending the expressive power of key paths to method and initializer members.
Key path expressions can refer to instance methods, type methods and initializers, and imitate the syntax of non-key path member references.
Key paths can reference methods in two forms:
- Without argument application: The key path represents the unapplied method signature.
- With argument application: The key path references the method with arguments already applied.
Continuing our Calculator
example, we can write either:
let squareWithoutArgs: KeyPath<Calculator, (Int) -> Int> = \Calculator.square
let squareWithArgs: KeyPath<Calculator, Int> = \Calculator.square(of: 3)
If the member is a metatype (e.g., a static method, class method, initializer, or when referring to the type of an instance), you must explicitly include .Type
in the key path root 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)
Here, addKeyPath
is a key path that references the add method of Calculator
as a metatype member. The key path’s root type is Calculator.Type
, and the value resolves to an applied instance method result type ofInt
.
Keypaths to methods with the same base name and distinct argument labels can be disambiguated with explicit argument labels:
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>
This feature also supports implicit closure conversion of key path methods, allowing them to used in expressions where closures are expected, such as in higher order functions:
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
can resolve method references through key paths, allowing methods to be accessed dynamically without explicit function calls:
@dynamicMemberLookup
struct DynamicKeyPathWrapper<Root> {
var root: Root
subscript<Member>(dynamicMember keyPath: KeyPath<Root, Member>) -> Member {
root[keyPath: keyPath]
}
}
let dynamicCalculator = DynamicKeyPathWrapper(root: Calculator())
let subtract = dynamicCalculator.subtract
print(subtract(10))
Methods annotated with nonisolated
and consuming
are supported by this feature. However, noncopying root and value types are not supported. mutating
, throws
and async
are not supported for any other component type and will similarly not be supported for methods. Additionally keypaths cannot capture closure arguments that are not Hashable
/Equatable
.
Component chaining between methods or from method to other key path types is also supported with this feature and will continue to behave as Hashable
/Equatable
types.
let kp5 = \Calculator.subtract(this: 1).signum()
let kp6 = \Calculator.subtract(this: 2).description
This feature has no effect on source compatibility.
This feature does not affect ABI compatibility.
This feature has no implications on adoption.