Skip to content

Commit 0685b28

Browse files
authored
Merge pull request #33119 from owenv/closure-matching-note
Add an educational note explaining SE-0286 changes
2 parents cc01438 + bacc96b commit 0685b28

File tree

2 files changed

+106
-1
lines changed

2 files changed

+106
-1
lines changed

include/swift/AST/EducationalNotes.def

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ EDUCATIONAL_NOTES(append_interpolation_static,
7676
"string-interpolation-conformance.md")
7777
EDUCATIONAL_NOTES(append_interpolation_void_or_discardable,
7878
"string-interpolation-conformance.md")
79-
EDUCATIONAL_NOTES(type_cannot_conform, "protocol-type-non-conformance.md")
79+
EDUCATIONAL_NOTES(type_cannot_conform, "protocol-type-non-conformance.md")
80+
81+
EDUCATIONAL_NOTES(unlabeled_trailing_closure_deprecated,
82+
"trailing-closure-matching.md")
8083

8184
#undef EDUCATIONAL_NOTES
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Argument Matching for Trailing Closures
2+
3+
In Swift, calling a function with one or more trailing closure arguments requires the label of the first trailing closure argument to be omitted. As a result, the compiler must consider additional context when determining which function parameter the trailing closure should satisfy.
4+
5+
Before Swift 5.3, the compiler used a backward scanning rule to match a trailing closure to a function parameter. Starting from the end of the parameter list, it moved backwards until finding a parameter which could accept a trailing closure argument (a function type, unconstrained generic parameter, `Any`, etc.). This could sometimes lead to unexpected behavior. Consider the following example:
6+
7+
```swift
8+
func animate(
9+
withDuration duration: Double,
10+
animations: () -> Void,
11+
completion: (() -> Void)? = nil
12+
) {}
13+
14+
// OK
15+
animate(withDuration: 0.3, animations: { /* Animate Something */ }) {
16+
// Done!
17+
}
18+
19+
// error: missing argument for parameter 'animations' in call
20+
animate(withDuration: 0.3) {
21+
// Animate Something
22+
}
23+
```
24+
25+
The second call to `animate` results in a compiler error because the backward scanning rule matches the trailing closure to the `completion` parameter instead of `animations`.
26+
27+
Beginning in Swift 5.3, the compiler uses a new, forward scanning rule to match trailing closures to function parameters. When matching function arguments to parameters, the forward scan first matches all non-trailing arguments from left-to-right. Then, it continues matching trailing closures in a left-to-right manner. This leads to more predictable and easy-to-understand behavior in many situations. With the new rule, the example above now works as expected without any modifications:
28+
29+
```swift
30+
// Remains valid
31+
animate(withDuration: 0.3, animations: { /* Animate Something */ }) {
32+
// Done!
33+
}
34+
35+
// Also OK!
36+
animate(withDuration: 0.3) {
37+
// Animate Something
38+
}
39+
```
40+
41+
When scanning forwards to match an unlabeled trailing closure argument, the compiler may sometimes need to "skip over" defaulted and variadic arguments. The new rule will skip any parameter that does not structurally resemble a function type. This allows writing a modified version of the above example where `withDuration` also has a default argument value:
42+
43+
```swift
44+
func animate(
45+
withDuration duration: Double = 1.0,
46+
animations: () -> Void,
47+
completion: (() -> Void)? = nil
48+
) {}
49+
50+
// Allowed! The forward scanning rule skips `withDuration` because it does not
51+
// structurally resemble a function type.
52+
animate {
53+
// Animate Something
54+
}
55+
```
56+
57+
A parameter structurally resembles a function type if both of the following are true:
58+
59+
- The parameter is not `inout`
60+
- The adjusted type of the parameter is a function type
61+
62+
The adjusted type of the parameter is the parameter's type as it appears in the function declaration, looking through any type aliases, and performing three additional adjustments:
63+
64+
- If the parameter is an `@autoclosure`, using the result type of the parameter's declared (function) type, before performing the second adjustment.
65+
- If the parameter is variadic, looking at the base element type.
66+
- Removing all outer "optional" types.
67+
68+
To maintain source compatibility with code that was written before Swift 5.3, the forward scanning rule applies an additional heuristic when matching trailing closure arguments. If,
69+
70+
- the parameter that would match an unlabeled trailing closure argument according to the forward scanning rule does not require an argument (because it is variadic or has a default argument), _and_
71+
- there are parameters _following_ that parameter that _do_ require an argument, which appear before the first parameter whose label matches that of the _next_ trailing closure (if any)
72+
73+
then the compiler does not match the unlabeled trailing closure to that parameter. Instead, it skips it and examines the next parameter to see if that should be matched against the unlabeled trailing closure. This can be seen in the following example:
74+
75+
```swift
76+
func showAlert(
77+
message: String,
78+
onPresentation: (() -> Void)? = nil,
79+
onDismissal: () -> Void
80+
) {}
81+
82+
// The unlabeled trailing closure matches `onDismissal` because `onPresentation`
83+
// does not require an argument, but `onDismissal` does and there are no other
84+
// trailing closures which could match it.
85+
showAlert(message: "Hello, World!") {
86+
// On dismissal action
87+
}
88+
89+
// The unlabeled trailing closure matches `onPresentation` because although
90+
// `onPresentation` does not require an argument, there are no parameters
91+
// following it which require an argument and appear before the parameter
92+
// whose label matches the next trailing closure argument (`onDismissal`).
93+
showAlert(message: "Hello, World!") {
94+
// On presentation action
95+
} onDismissal: {
96+
// On dismissal action
97+
}
98+
```
99+
100+
Additionally, the Swift 5 compiler will attempt to apply both the new forward scanning rule and the old backward scanning rule when it encounters a call with a single trailing closure. If the forward and backward scans produce *different* valid assignments of arguments to parameters, the compiler will prefer the result of the backward scanning rule and produce a warning.
101+
102+
To learn more about argument matching for trailing closures, see [Swift Evolution Proposal SE-0286](https://github.com/apple/swift-evolution/blob/master/proposals/0286-forward-scan-trailing-closures.md).

0 commit comments

Comments
 (0)