Skip to content

Commit 1190985

Browse files
itaiferberxwu
andauthored
Allow Additional Arguments to @dynamicMemberLookup Subscripts (#2814)
* Add proposal for Allow Additional Arguments to `@dynamicMemberLookup` Subscripts * Incorporate pitch thread feedback * Update review manager * Assign SE-0484 --------- Co-authored-by: Xiaodi Wu <[email protected]>
1 parent 408dd16 commit 1190985

File tree

1 file changed

+281
-0
lines changed

1 file changed

+281
-0
lines changed
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
# Allow Additional Arguments to `@dynamicMemberLookup` Subscripts
2+
3+
* Proposal: [SE-0484](0484-allow-additional-args-to-dynamicmemberlookup-subscripts.md)
4+
* Authors: [Itai Ferber](https://github.com/itaiferber)
5+
* Review Manager: [Xiaodi Wu](https://github.com/xwu)
6+
* Status: **Active review (May 12...25, 2025)**
7+
* Implementation: [swiftlang/swift#81148](https://github.com/swiftlang/swift/pull/81148)
8+
* Previous Proposals: [SE-0195](0195-dynamic-member-lookup.md), [SE-0252](0252-keypath-dynamic-member-lookup.md)
9+
* Review: ([pitch](https://forums.swift.org/t/pitch-allow-additional-arguments-to-dynamicmemberlookup-subscripts/79558))
10+
11+
## Introduction
12+
13+
SE-0195 and SE-0252 introduced and refined `@dynamicMemberLookup` to provide type-safe "dot"-syntax access to arbitrary members of a type by reflecting the existence of certain `subscript(dynamicMember:)` methods on that type, turning
14+
15+
```swift
16+
let _ = x.member
17+
x.member = 42
18+
ƒ(&x.member)
19+
```
20+
21+
into
22+
23+
```swift
24+
let _ = x[dynamicMember: <member>]
25+
x[dynamicMember: <member>] = 42
26+
ƒ(&x[dynamicMember: <member>])
27+
```
28+
29+
when `x.member` doesn't otherwise exist statically. Currently, in order to be eligible to satisfy `@dynamicMemberLookup` requirements, a subscript must:
30+
31+
1. Take _exactly one_ argument with an explicit `dynamicMember` argument label,
32+
2. Whose type is non-variadic and is either
33+
* A `{{Reference}Writable}KeyPath`, or
34+
* A concrete type conforming to `ExpressibleByStringLiteral`
35+
36+
This proposal intends to relax the "exactly one" requirement above to allow eligible subscripts to take additional arguments after `dynamicMember` as long as they have a default value (or are variadic, and thus have an implicit default value).
37+
38+
## Motivation
39+
40+
Dynamic member lookup is often used to provide expressive and succinct API in wrapping some underlying data, be it a type-erased foreign language object (e.g., a Python `PyVal` or a JavaScript `JSValue`) or a native Swift type. This (and [`callAsFunction()`](0253-callable.md)) allow a generalized API interface such as
41+
42+
```swift
43+
struct Value {
44+
subscript(_ property: String) -> Value {
45+
get { ... }
46+
set { ... }
47+
}
48+
49+
func invoke(_ method: String, _ args: Any...) -> Value {
50+
...
51+
}
52+
}
53+
54+
let x: Value = ...
55+
let _ = x["member"]
56+
x["member"] = Value(42)
57+
x.invoke("someMethod", 1, 2, 3)
58+
```
59+
60+
to be expressed much more naturally:
61+
62+
```swift
63+
@dynamicMemberLookup
64+
struct Value {
65+
struct Method {
66+
func callAsFunction(_ args: Any...) -> Value { ... }
67+
}
68+
69+
subscript(dynamicMember property: String) -> Value {
70+
get { ... }
71+
set { ... }
72+
}
73+
74+
subscript(dynamicMember method: String) -> Method { ... }
75+
}
76+
77+
let x: Value = ...
78+
let _ = x.member
79+
x.member = Value(42)
80+
x.someMethod(1, 2, 3)
81+
```
82+
83+
However, as wrappers for underlying data, sometimes interfaces like this need to be able to "thread through" additional information. For example, it might be helpful to provide information about call sites for debugging purposes:
84+
85+
```swift
86+
struct Value {
87+
subscript(
88+
_ property: String,
89+
function: StaticString = #function,
90+
file: StaticString = #fileID,
91+
line: UInt = #line
92+
) -> Value {
93+
...
94+
}
95+
96+
func invokeMethod(
97+
_ method: String,
98+
function: StaticString = #function,
99+
file: StaticString = #fileID,
100+
line: UInt = #line,
101+
_ args: Any...
102+
) -> Value {
103+
...
104+
}
105+
}
106+
```
107+
108+
When additional arguments like this have default values, they don't affect the appearance of call sites at all:
109+
110+
```swift
111+
let x: Value = ...
112+
let _ = x["member"]
113+
x["member"] = Value(42)
114+
x.invoke("someMethod", 1, 2, 3)
115+
```
116+
117+
However, these are not valid for use with dynamic member lookup subscripts, since the additional arguments prevent subscripts from being eligible for dynamic member lookup:
118+
119+
```swift
120+
@dynamicMemberLookup // error: @dynamicMemberLookupAttribute requires 'Value' to have a 'subscript(dynamicMember:)' method that accepts either 'ExpressibleByStringLiteral' or a key path
121+
struct Value {
122+
subscript(
123+
dynamicMember property: String,
124+
function: StaticString = #function,
125+
file: StaticString = #fileID,
126+
line: UInt = #line
127+
) -> Value {
128+
...
129+
}
130+
131+
subscript(
132+
dynamicMember method: String,
133+
function: StaticString = #function,
134+
file: StaticString = #fileID,
135+
line: UInt = #line
136+
) -> Method {
137+
...
138+
}
139+
}
140+
```
141+
142+
## Proposed solution
143+
144+
We can amend the rules for such subscripts to make them eligible. With this proposal, in order to be eligible to satisfy `@dynamicMemberLookup` requirements, a subscript must:
145+
146+
1. Take an initial argument with an explicit `dynamicMember` argument label,
147+
2. Whose parameter type is non-variadic and is either:
148+
* A `{{Reference}Writable}KeyPath`, or
149+
* A concrete type conforming to `ExpressibleByStringLiteral`,
150+
3. And whose following arguments (if any) are all either variadic or have a default value
151+
152+
## Detailed design
153+
154+
Since compiler support for dynamic member lookup is already robust, implementing this requires primarily:
155+
156+
1. Type-checking of `@dynamicMemberLookup`-annotated declarations to also consider `subscript(dynamicMember:...)` methods following the above rules as valid, and
157+
2. Syntactic transformation of `T.<member>` to `T[dynamicMember:...]` in the constraint system to fill in default arguments expressions for any following arguments
158+
159+
## Source compatibility
160+
161+
This is largely an additive change with minimal impact to source compatibility. Types which do not opt in to `@dynamicMemberLookup` are unaffected, as are types which do opt in and only offer `subscript(dynamicMember:)` methods which take a single argument.
162+
163+
However, types which opt in to `@dynamicMemberLookup` and currently offer an overload of `subscript(dynamicMember:...)`—which today is not eligible for consideration for dynamic member lookup—_may_ now select this overload when they wouldn't have before.
164+
165+
### Overload resolution
166+
167+
Dynamic member lookups go through regular overload resolution, with an additional disambiguation rule that prefers keypath-based subscript overloads over string-based ones. Since the `dynamicMember` argument to dynamic member subscripts is implicit, overloads of `subscript(dynamicMember:)` are primarily selected based on their return type (and typically for keypath-based subscripts, how that return type is used in forming the type of a keypath parameter).
168+
169+
With this proposal, all arguments to `subscript(dynamicMember:...)` are still implicit, so overloads are still primarily selected based on return type, with the additional disambiguation rule that prefers overloads with fewer arguments over overloads with more arguments. (This rule applies "for free" since it already applies to method calls, which dynamic member lookups are transformed into.)
170+
171+
This means that if a type today offers a valid `subscript(dynamicMember:) -> T` and a (currently-unconsidered) `subscript(dynamicMember:...) -> U`,
172+
173+
1. If `T == U` then the former will still be the preferred overload in all circumstances
174+
2. If `T` and `U` are compatible (and equally-specific) at a callsite then the former will still be the preferred overload
175+
3. If `T` and `U` are incompatible, or if one is more specific than the other, then the more specific type will be preferred
176+
177+
For example:
178+
179+
```swift
180+
@dynamicMemberLookup
181+
struct A {
182+
/* (1) */ subscript(dynamicMember member: String) -> String { ... }
183+
/* (2) */ subscript(dynamicMember member: String, _: StaticString = #function) -> String { ... }
184+
}
185+
186+
@dynamicMemberLookup
187+
struct B {
188+
/* (3) */ subscript(dynamicMember member: String) -> String { ... }
189+
/* (4) */ subscript(dynamicMember member: String, _: StaticString = #function) -> Int { ... }
190+
}
191+
192+
@dynamicMemberLookup
193+
struct C {
194+
/* (5) */ subscript(dynamicMember member: String) -> String { ... }
195+
/* (6) */ subscript(dynamicMember member: String, _: StaticString = #function) -> String? { ... }
196+
}
197+
198+
// T == U
199+
let _ = A().member // (1) preferred over (2); no ambiguity
200+
let _: String = A().member // (1) preferred over (2); no ambiguity
201+
202+
// T and U are compatible
203+
let _: Any = A().member // (1) preferred over (2); no ambiguity
204+
let _: Any = B().member // (3) preferred over (4); no ambiguity
205+
let _: Any = C().member // (5) preferred over (6); no ambiguity
206+
207+
// T and U are incompatible/differently-specific
208+
let _: String = B().member // (3)
209+
let _: Int = B().member // (4);️ would not previously compile
210+
let _: String = C().member // (5); no ambiguity
211+
let _: String? = C().member // (6) preferred over (5); ⚠️ previously (5) ⚠️
212+
```
213+
214+
This last case is the only source of behavior change: (6) was previously not considered a valid candidate, but has a return type more specific than (5), and is now picked at a callsite.
215+
216+
In practice, it is expected that this situation is exceedingly rare.
217+
218+
## ABI compatibility
219+
220+
This feature is implemented entirely in the compiler as a syntactic transformation and has no impact on the ABI.
221+
222+
## Implications on adoption
223+
224+
The changes in this proposal require the adoption of a new version of the Swift compiler.
225+
226+
## Alternatives considered
227+
228+
The main alternative to this proposal is to not implement it, as:
229+
1. It was noted in [the pitch thread](https://forums.swift.org/t/pitch-allow-additional-arguments-to-dynamicmemberlookup-subscripts/79558) that allowing additional arguments to dynamic member lookup widens the gap in capabilities between dynamic members and regular members — dynamic members would be able to
230+
231+
1. Have caller side effects (i.e., have access to `#function`, `#file`, `#line`, etc.),
232+
2. Constrain themselves via generics, and
233+
3. Apply isolation to themselves via `#isolation`
234+
235+
where regular members cannot. However, (i) and (iii) are not considered an imbalance in functionality but instead are the raison d'être of this proposal. (ii) is also already possible today as dynamic member subscripts can be constrained via generics (and this is often used with keypath-based lookup).
236+
2. This is possible to work around using explicit methods such as `get()` and `set(_:)`:
237+
238+
```swift
239+
@dynamicMemberLookup
240+
struct Value {
241+
struct Property {
242+
func get(
243+
function: StaticString = #function,
244+
file: StaticString = #file,
245+
line: UInt = #line
246+
) -> Value {
247+
...
248+
}
249+
250+
func set(
251+
_ value: Value,
252+
function: StaticString = #function,
253+
file: StaticString = #file,
254+
line: UInt = #line
255+
) {
256+
...
257+
}
258+
}
259+
260+
subscript(dynamicMember member: String) -> Property { ... }
261+
}
262+
263+
let x: Value = ...
264+
let _ = x.member.get() // x.member
265+
x.member.set(Value(42)) // x.member = Value(42)
266+
```
267+
268+
However, this feels non-idiomatic, and for long chains of getters and setters, can become cumbersome:
269+
270+
```swift
271+
let x: Value = ...
272+
let _ = x.member.get().inner.get().nested.get() // x.member.inner.nested
273+
x.member.get().inner.get().nested.set(Value(42)) // x.member.inner.nested = Value(42)
274+
```
275+
276+
### Source compatibility
277+
278+
It is possible to avoid the risk of the behavior change noted above by adjusting the constraint system to always prefer `subscript(dynamicMember:) -> T` overloads over `subscript(dynamicMember:...) -> U` overloads (if `T` and `U` are compatible), even if `U` is more specific than `T`. However,
279+
280+
1. This would be a departure from the normal method overload resolution behavior that Swift developers are familiar with, and
281+
2. If `T` were a supertype of `U`, it would be impossible to ever call the more specific overload except by direct subscript access

0 commit comments

Comments
 (0)