Skip to content

Commit 6c734a0

Browse files
dan-zhenglattner
authored andcommitted
Clarify SE-2016: user-defined dynamically callable types. (#948)
* Clarify SE-2016: user-defined dynamically callable types. - Clarify the dynamic call resolution strategy: - Dynamic calls are resolved based on call-site syntax. - Use the `withArguments:` method if it's defined and there are no keyword arguments. - Otherwise, use the `withKeywordArguments:` method. - Update the implementation PR link. * Minor edits. Address comments by @benrimmington. * Minor edits. Address comments by @benrimmington.
1 parent fb85e67 commit 6c734a0

File tree

1 file changed

+100
-48
lines changed

1 file changed

+100
-48
lines changed

proposals/0216-dynamic-callable.md

Lines changed: 100 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
* Proposal: [SE-0216](0216-dynamic-callable.md)
44
* Authors: [Chris Lattner](https://github.com/lattner), [Dan Zheng](https://github.com/dan-zheng)
55
* Review Manager: [John McCall](https://github.com/rjmccall)
6-
* Implementation: [apple/swift#16980](https://github.com/apple/swift/pull/16980)
6+
* Implementation: [apple/swift#20305](https://github.com/apple/swift/pull/20305)
77
* Decision Notes: [Rationale](https://forums.swift.org/t/accepted-se-216-user-defined-dynamically-callable-types/14110)
88
* Status: **Accepted**
99

1010
## Introduction
1111

12-
This proposal is a follow-on to [SE-0195 - Introduce User-defined "Dynamic Member
13-
Lookup" Types](https://github.com/apple/swift-evolution/blob/master/proposals/0195-dynamic-member-lookup.md)
12+
This proposal is a follow-up to [SE-0195 - Introduce User-defined "Dynamic Member
13+
Lookup" Types](https://github.com/apple/swift-evolution/blob/master/proposals/0195-dynamic-member-lookup.md),
1414
which shipped in Swift 4.2. It introduces a new `@dynamicCallable` attribute, which marks
1515
a type as being "callable" with normal syntax. It is simple syntactic sugar
1616
which allows the user to write:
@@ -19,7 +19,7 @@ which allows the user to write:
1919
a = someValue(keyword1: 42, "foo", keyword2: 19)
2020
````
2121

22-
and have it be interpreted by the compiler as:
22+
and have it be rewritten by the compiler as:
2323

2424
```swift
2525
a = someValue.dynamicallyCall(withKeywordArguments: [
@@ -113,7 +113,7 @@ This capability works well, but the syntactic burden of having to use
113113
the syntactic weight, it directly harms code clarity by making code hard to
114114
read and understand, cutting against a core value of Swift.
115115

116-
The `@dynamicCallable` attribute in this proposal directly solves this problem.
116+
The proposed `@dynamicCallable` attribute directly solves this problem.
117117
With it, these examples become more natural and clear, effectively matching the
118118
original Python code in expressiveness:
119119

@@ -147,13 +147,14 @@ let blob = file.read()
147147
let result = pickle.loads(blob)
148148
```
149149

150-
This is a proposal is purely syntactic sugar - it introduces no new semantic
151-
model to Swift at all. We believe that interoperability with scripting
150+
This proposal merely introduces a syntactic sugar - it does not add any new
151+
semantic model to Swift. We believe that interoperability with scripting
152152
languages is an important and rising need in the Swift community, particularly
153153
as Swift makes inroads into the server development and machine learning
154-
communities. This sort of capability is also highly precedented in other
155-
languages, and is a generally useful language feature that could be used for
156-
other purposes as well (e.g. for implementing dynamic proxy objects).
154+
communities. This feature is also precedented in other languages (e.g. Scala's
155+
[`Dynamic`](https://www.scala-lang.org/api/current/scala/Dynamic.html) trait), and
156+
can be used for other purposes besides language interoperability (e.g.
157+
implementing dynamic proxy objects).
157158

158159
## Proposed solution
159160

@@ -162,45 +163,53 @@ which may be applied to structs, classes, enums, and protocols. This follows
162163
the precedent of
163164
[SE-0195](https://github.com/apple/swift-evolution/blob/master/proposals/0195-dynamic-member-lookup.md).
164165

165-
Before this proposal, values of these types are not valid in a function call
166-
position: the only callable values that Swift has are those with a function
167-
type (functions, methods, closures, etc) and metatypes (which are initializer
166+
Before this proposal, values of these types are not valid in a call expression:
167+
the only existing callable values in Swift are those with function types
168+
(functions, methods, closures, etc) and metatypes (which are initializer
168169
expressions like `String(42)`). Thus, it is always an error to "call" an
169170
instance of a nominal type (like a struct, for instance).
170171

171-
With this proposal, types that adopt the new attribute on their primary type
172-
declaration become "callable" and are required to implement one or more methods
173-
for handling the call behavior.
174-
175-
To support these cases, a type with this attribute is required to implement at
176-
least one of the following two methods. In the examples below, `T*` are
177-
arbitrary types, `S*` must conform to `ExpressibleByStringLiteral` (e.g.
178-
`String` and `StaticString`).
172+
With this proposal, types with the `@dynamicCallable` attribute on their
173+
primary type declaration become "callable". They are required to implement at
174+
least one of the following two methods for handling the call behavior:
179175

180176
```swift
181-
func dynamicallyCall(withArguments: [T1]) -> T2
182-
func dynamicallyCall(withKeywordArguments: [S : T3]) -> T4
177+
func dynamicallyCall(withArguments: <#Arguments#>) -> <#R1#>
178+
// `<#Arguments#>` can be any type that conforms to `ExpressibleByArrayLiteral`.
179+
// `<#Arguments#>.ArrayLiteralElement` and the result type `<#R1#>` can be arbitrary.
180+
181+
func dynamicallyCall(withKeywordArguments: <#KeywordArguments#>) -> <#R2#>
182+
// `<#KeywordArguments#>` can be any type that conforms to `ExpressibleByDictionaryLiteral`.
183+
// `<#KeywordArguments#>.Key` must be a type that conforms to `ExpressibleByStringLiteral`.
184+
// `<#KeywordArguments#>.Value` and the result type `<#R2#>` can be arbitrary.
185+
186+
// Note: in these type signatures, bracketed types like <#Arguments#> and <#KeywordArguments#>
187+
// are not actual types, but rather any actual type that meets the specified conditions.
183188
```
184189

185-
We write `Arguments` as an array type and `KeywordArguments` as a dictionary
186-
type, but these can actually be any type that conforms to the
187-
`ExpressibleByArrayLiteral` and `ExpressibleByDictionaryLiteral` protocols,
188-
respectively. The later is inclusive of
189-
[`KeyValuePairs`](https://developer.apple.com/documentation/swift/keyvaluepairs)
190-
which can represent multiple instances of a 'key' in the collection. This is
191-
important to support duplicated and positional arguments (because positional
192-
arguments have the empty string `""` as their key).
190+
As stated above, `<#Arguments#>` and `<#KeywordArguments#>` can be any types
191+
that conform to the
192+
[`ExpressibleByArrayLiteral`](https://developer.apple.com/documentation/swift/expressiblebyarrayliteral)
193+
and
194+
[`ExpressibleByDictionaryLiteral`](https://developer.apple.com/documentation/swift/expressiblebydictionaryliteral)
195+
protocols, respectively. The latter is inclusive of
196+
[`KeyValuePairs`](https://developer.apple.com/documentation/swift/keyvaluepairs),
197+
which supports duplicate keys, unlike [`Dictionary`](https://developer.apple.com/documentation/swift/dictionary).
198+
Thus, using `KeyValuePairs` is recommended to support duplicate keywords and
199+
positional arguments (because positional arguments are desugared as keyword
200+
arguments with the empty string `""` as the key).
193201

194202
If a type implements the `withKeywordArguments:` method, it may be dynamically
195-
called with both positional and keyword arguments (positional arguments have
196-
the empty string `""` as their key). If a type only implements the
203+
called with both positional and keyword arguments: positional arguments have
204+
the empty string `""` as the key. If a type only implements the
197205
`withArguments:` method but is called with keyword arguments, a compile-time
198206
error is emitted.
199207

200-
Because this is a syntactic sugar proposal, additional behavior of the
201-
implementation methods is directly expressed: for example, if these types are
202-
defined to be `throws` or `@discardableResult` then the corresponding sugared
203-
call is as well.
208+
Since dynamic calls are syntactic sugar for direct calls to `dynamicallyCall`
209+
methods, additional behavior of the `dynamicallyCall` methods is directly
210+
forwarded. For example, if a `dynamicallyCall` method is marked with `throws`
211+
or `@discardableResult`, then the corresponding sugared dynamic call will
212+
forward that behavior.
204213

205214
### Ambiguity resolution: most specific match
206215

@@ -209,17 +218,60 @@ handle some dynamic calls. What happens if a type specifies both the
209218
`withArguments:` and `withKeywordArguments:` methods?
210219

211220
We propose that the type checker resolve this ambiguity towards the tightest
212-
match based on syntactic form of the expression. If a type implements both the
213-
`withArguments:` and `withKeywordArguments:` methods, the compiler will use the
214-
`withArguments:` method for call sites that have no keyword arguments and the
215-
`withKeywordArguments:` method for call sites that have at least one keyword
216-
argument.
217-
218-
This ambiguity resolution rule works out very naturally given the behavior of
219-
the Swift type checker, because it only resolves call expressions when the type
220-
of the base expression is known. At that point, it knows the capabilities of
221-
the type (whether the base is a function type, metatype, or a valid
222-
`@dynamicCallable` type where one of these two methods exist) and it knows the
221+
match based on syntactic form of the expression. The exact rules are:
222+
223+
- If a `@dynamicCallable` type implements the `withArguments:` method and it is
224+
called with no keyword arguments, use the `withArguments:` method.
225+
- In all other cases, attempt to use the `withKeywordArguments:` method.
226+
- This includes the case where a `@dynamicCallable` type implements the
227+
`withKeywordArguments:` method and it is called with at least one keyword
228+
argument.
229+
- This also includes the case where a `@dynamicCallable` type implements only
230+
the `withKeywordArguments:` method (not the `withArguments:` method) and
231+
it is called with no keyword arguments.
232+
- If `@dynamicCallable` type does not implement the `withKeywordArguments:`
233+
method but the call site has keyword arguments, an error is emitted.
234+
235+
Here are some toy illustrative examples:
236+
237+
```swift
238+
@dynamicCallable
239+
struct Callable {
240+
func dynamicallyCall(withArguments args: [Int]) -> Int { return args.count }
241+
}
242+
let c1 = Callable()
243+
c1() // desugars to `c1.dynamicallyCall(withArguments: [])`
244+
c1(1, 2) // desugars to `c1.dynamicallyCall(withArguments: [1, 2])`
245+
c1(a: 1, 2) // error: `Callable` does not define the 'withKeywordArguments:' method
246+
247+
@dynamicCallable
248+
struct KeywordCallable {
249+
func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Int {
250+
return args.count
251+
}
252+
}
253+
let c2 = KeywordCallable()
254+
c2() // desugars to `c2.dynamicallyCall(withKeywordArguments: [:])`
255+
c2(1, 2) // desugars to `c2.dynamicallyCall(withKeywordArguments: ["": 1, "": 2])`
256+
c2(a: 1, 2) // desugars to `c2.dynamicallyCall(withKeywordArguments: ["a": 1, "": 2])`
257+
258+
@dynamicCallable
259+
struct BothCallable {
260+
func dynamicallyCall(withArguments args: [Int]) -> Int { return args.count }
261+
func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Int {
262+
return args.count
263+
}
264+
}
265+
let c3 = BothCallable()
266+
c3() // desugars to `c3.dynamicallyCall(withArguments: [])`
267+
c3(1, 2) // desugars to `c3.dynamicallyCall(withArguments: [1, 2])`
268+
c3(a: 1, 2) // desugars to `c3.dynamicallyCall(withKeywordArguments: ["a": 1, "": 2])`
269+
```
270+
271+
This ambiguity resolution rule works out naturally given the behavior of the
272+
Swift type checker, because it only resolves call expressions when the type
273+
of the base expression is known. At that point, it knows whether the base is a
274+
function type, metatype, or a valid `@dynamicCallable` type, and it knows the
223275
syntactic form of the call.
224276

225277
This proposal does not require massive or invasive changes to the constraint

0 commit comments

Comments
 (0)