Skip to content

Commit ef5b967

Browse files
committed
Add proposal text.
1 parent a0fb99f commit ef5b967

File tree

1 file changed

+165
-0
lines changed

1 file changed

+165
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# Method and Initializer Key Paths
2+
3+
* Proposal: [SE-NNNN](NNNN-filename.md)
4+
* Authors: [Amritpan Kaur](https://github.com/amritpan), [Pavel Yaskevich](https://github.com/xedin)
5+
* Review Manager: TBD
6+
* Status: **Awaiting review**
7+
* Implementation: [swiftlang/swift#78823](https://github.com/swiftlang/swift/pull/78823) [swiftsyntax/swiftsyntax#2950](https://github.com/swiftlang/swift-syntax/pull/2950)
8+
* Upcoming Feature Flag: *if applicable* `KeyPathWithMethodMembers`
9+
* Review: ([pitch](https://forums.swift.org/t/pitch-method-key-paths/76678))
10+
11+
## Introduction
12+
13+
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.
14+
15+
## Motivation
16+
17+
Key paths to method members and their advantages have been explored in several discussions on the Swift forum, specifically to [unapplied instance methods](https://forums.swift.org/t/allow-key-paths-to-reference-unapplied-instance-methods/35582) and to [partially and applied methods](https://forums.swift.org/t/pitch-allow-keypaths-to-represent-functions/67630). 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.
18+
19+
## Proposed solution
20+
21+
We propose the following usage:
22+
23+
```swift
24+
struct Calculator {
25+
func square(of number: Int) -> Int {
26+
return number * number * multiplier
27+
}
28+
29+
func cube(of number: Int) -> Int {
30+
return number * number * number * multiplier
31+
}
32+
33+
init(multiplier: Int) {
34+
self.multiplier = multiplier
35+
}
36+
37+
let multiplier: Int
38+
}
39+
40+
// Key paths to Calculator methods
41+
let squareKeyPath = \Calculator.square
42+
let cubeKeyPath = \Calculator.cube
43+
```
44+
45+
These key paths can then be invoked dynamically with a generic function:
46+
47+
```swift
48+
func invoke<T, U>(object: T, keyPath: KeyPath<T, (U) -> U>, param: U) -> U {
49+
return object[keyPath: keyPath](param)
50+
}
51+
52+
let calc = Calculator(multiplier: 2)
53+
54+
let squareResult = invoke(object: calc, keyPath: squareKeyPath, param: 3)
55+
let cubeResult = invoke(object: calc, keyPath: cubeKeyPath, param: 3)
56+
```
57+
58+
Or used to dynamically create a new instance of Calculator:
59+
60+
```swift
61+
let initializerKeyPath = \Calculator.Type.init(multiplier: 5)
62+
```
63+
64+
This proposed feature homogenizes the treatment of member declarations by extending the expressive power of key paths to method and initializer members.
65+
66+
## Detailed design
67+
68+
Key path expressions can refer to instance methods, type methods and initializers, and imitate the syntax of non-key path member references.
69+
70+
### Argument Application
71+
72+
Key paths can reference methods in two forms:
73+
74+
1. Without argument application: The key path represents the unapplied method signature.
75+
2. With argument application: The key path references the method with arguments already applied.
76+
77+
Continuing our `Calculator` example, we can write either:
78+
79+
```swift
80+
let squareWithoutArgs: KeyPath<Calculator, (Int) -> Int> = \Calculator.square
81+
let squareWithArgs: KeyPath<Calculator, Int> = \Calculator.square(of: 3)
82+
```
83+
84+
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.
85+
86+
```swift
87+
struct Calculator {
88+
func add(_ a: Int, _ b: Int) -> Int {
89+
return a + b
90+
}
91+
}
92+
93+
let calc = Calculator.self
94+
let addKeyPath: KeyPath<Calculator.Type, (Calculator) -> (Int, Int) -> Int> = \Calculator.Type.add
95+
```
96+
97+
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 it resolves to an unapplied instance method: `(Calculator) -> (Int, Int) -> Int`. This represents a curried function where the first step binds an instance of `Calculator`, and the second step applies the method arguments.
98+
99+
```swift
100+
let addFunction = calc[keyPath: addKeyPath]
101+
let fullyApplied = addFunction(Calculator())(20, 30)`
102+
```
103+
104+
`addFunction` applies an instance of Calculator to the key path method. `fullyApplied` further applies the arguments (20, 30) to produce the final result.
105+
106+
### Overloads
107+
108+
Keypaths to methods with the same base name and distinct argument labels can be disambiguated by explicitly including the argument labels:
109+
110+
```swift
111+
struct Calculator {
112+
var subtract: (Int, Int) -> Int { return { $0 + $1 } }
113+
func subtract(this: Int) -> Int { this + this}
114+
func subtract(that: Int) -> Int { that + that }
115+
}
116+
117+
let kp1 = \S.subtract // KeyPath<S, (Int, Int) -> Int
118+
let kp2 = \S.subtract(this:) // WritableKeyPath<S, (Int) -> Int>
119+
let kp3 = \S.subtract(that:) // WritableKeyPath<S, (Int) -> Int>
120+
let kp4 = \S.subtract(that: 1) // WritableKeyPath<S, Int>
121+
```
122+
123+
### Dynamic member lookups
124+
125+
Dynamic member lookup can resolve method references through key paths, allowing methods to be accessed dynamically without explicit function calls:
126+
127+
```swift
128+
@dynamicMemberLookup
129+
struct DynamicKeyPathWrapper<Root> {
130+
var root: Root
131+
132+
subscript<Member>(dynamicMember keyPath: KeyPath<Root, Member>) -> Member {
133+
root[keyPath: keyPath]
134+
}
135+
}
136+
137+
let dynamicCalculator = DynamicKeyPathWrapper(root: Calculator())
138+
let subtract = dynamicCalculator.subtract
139+
print(subtract(10))
140+
```
141+
142+
### Effectful value types
143+
144+
Methods annotated with `nonisolated` and `consuming` are supported by this feature. `mutating`, `throwing` and `async` are not supported for any other component type and will similarly not be supported for methods. Keypaths cannot capture method arguments that are not `Hashable` or `Equatable`, so `escaping` is also not supported.
145+
146+
### Component chaining
147+
148+
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.
149+
150+
```swift
151+
let kp5 = \Calculator.subtract(this: 1).signum()
152+
let kp6 = \Calculator.subtract(this: 2).description
153+
```
154+
155+
## Source compatibility
156+
157+
This feature has no effect on source compatibility.
158+
159+
## ABI compatibility
160+
161+
This feature does not affect ABI compatibility.
162+
163+
## Implications on adoption
164+
165+
This feature has no implications on adoption.

0 commit comments

Comments
 (0)