|
1 | 1 | # Argument Matching for Trailing Closures
|
2 | 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: |
| 3 | +Where trailing closures are used to pass one or more arguments to a function, the argument label for the first trailing closure is always omitted: |
6 | 4 |
|
7 | 5 | ```swift
|
8 | 6 | func animate(
|
9 | 7 | withDuration duration: Double,
|
10 | 8 | animations: () -> Void,
|
11 | 9 | completion: (() -> Void)? = nil
|
12 |
| -) {} |
| 10 | +) { /* ... */ } |
13 | 11 |
|
14 |
| -// OK |
15 |
| -animate(withDuration: 0.3, animations: { /* Animate Something */ }) { |
16 |
| - // Done! |
| 12 | +animate(withDuration: 0.3) /* `animations:` is unwritten. */ { |
| 13 | + // Animate something. |
| 14 | +} completion: { |
| 15 | + // Completion handler. |
17 | 16 | }
|
| 17 | +``` |
18 | 18 |
|
19 |
| -// error: missing argument for parameter 'animations' in call |
| 19 | +Sometimes, an unlabeled trailing closure argument can be matched to more than one function parameter. Before Swift 5.3, the compiler would use a __backward scanning rule__ to match the unlabeled trailing closure, scanning backwards from the end of the parameter list until finding a parameter that can accept a closure argument (a function type, unconstrained generic type, `Any`, etc.): |
| 20 | + |
| 21 | +```swift |
20 | 22 | animate(withDuration: 0.3) {
|
21 |
| - // Animate Something |
| 23 | + // Animate something? |
| 24 | + // The compiler matches this to the `completion` parameter. |
22 | 25 | }
|
| 26 | +// error: missing argument for parameter 'animations' in call |
23 | 27 | ```
|
24 | 28 |
|
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`. |
| 29 | +We encounter a compiler error in this example because the backward scanning rule matches the trailing closure to the `completion` parameter instead of the `animations` parameter, even though `completion` has a default value while `animations` does not. |
26 | 30 |
|
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: |
| 31 | +Swift 5.3 introduces a new __forward scanning rule__, which matches trailing closures to function parameters from left to right (after matching non-trailing arguments). This leads to more predictable and easy-to-understand behavior in many situations. With the new rule, the unlabeled closure in the example above is matched to the `animations` parameter, just as most users would expect: |
28 | 32 |
|
29 | 33 | ```swift
|
30 |
| -// Remains valid |
31 |
| -animate(withDuration: 0.3, animations: { /* Animate Something */ }) { |
32 |
| - // Done! |
33 |
| -} |
34 |
| - |
35 |
| -// Also OK! |
36 | 34 | animate(withDuration: 0.3) {
|
37 |
| - // Animate Something |
38 |
| -} |
| 35 | + // Animate something. |
| 36 | +} // `completion` has the default value `nil`. |
39 | 37 | ```
|
40 | 38 |
|
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: |
| 39 | +When scanning forwards to match an unlabeled trailing closure argument, the compiler will skip any parameter that does not __structurally resemble__ a function type and also apply a __heuristic__ to skip parameters that do not require an argument in favor of a subsequent parameter that does (see below). These rules make possible the ergonomic use of a modified version of the API given in the example above, where `withDuration` has a default value: |
42 | 40 |
|
43 | 41 | ```swift
|
44 | 42 | func animate(
|
45 | 43 | withDuration duration: Double = 1.0,
|
46 | 44 | animations: () -> Void,
|
47 | 45 | completion: (() -> Void)? = nil
|
48 |
| -) {} |
| 46 | +) { /* ... */ } |
49 | 47 |
|
50 |
| -// Allowed! The forward scanning rule skips `withDuration` because it does not |
51 |
| -// structurally resemble a function type. |
52 | 48 | animate {
|
53 |
| - // Animate Something |
| 49 | + // Animate something. |
| 50 | + // |
| 51 | + // The closure is not matched to `withDuration` but to `animations` because |
| 52 | + // the first parameter doesn't structurally resemble a function type. |
| 53 | + // |
| 54 | + // If, in place of `withDuration`, there is a parameter with a default value |
| 55 | + // that does structurally resemble a function type, the closure would still be |
| 56 | + // matched to `animations` because it requires an argument while the first |
| 57 | + // parameter does not. |
54 | 58 | }
|
55 | 59 | ```
|
56 | 60 |
|
| 61 | +For source compatibility in Swift 5, the compiler will attempt to apply *both* the new forward scanning rule and the old backward scanning rule when it encounters a function call with a single trailing closure. If the forward and backward scans produce *different valid* matches of arguments to parameters, the compiler will prefer the result of the backward scanning rule and produce a warning. To silence this warning, rewrite the function call to label the argument explicitly without using trailing closure syntax. |
| 62 | + |
| 63 | +## Structural Resemblance to a Function Type |
| 64 | + |
57 | 65 | A parameter structurally resembles a function type if both of the following are true:
|
58 | 66 |
|
59 |
| -- The parameter is not `inout` |
60 |
| -- The adjusted type of the parameter is a function type |
| 67 | +- the parameter is not `inout`, and |
| 68 | +- the __adjusted type__ of the parameter is a function type. |
61 | 69 |
|
62 | 70 | 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 | 71 |
|
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. |
| 72 | +1. If the parameter is an `@autoclosure`, use the result type of the parameter's declared (function) type before performing the second adjustment. |
| 73 | +2. If the parameter is variadic, look at the base element type. |
| 74 | +3. Remove all outer "optional" types. |
| 75 | + |
| 76 | +## Heuristic for Skipping Parameters |
67 | 77 |
|
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, |
| 78 | +To maintain source compatibility, the forward scanning rule applies an additional heuristic when matching trailing closure arguments. If: |
69 | 79 |
|
70 | 80 | - 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) |
| 81 | +- 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 | 82 |
|
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: |
| 83 | +then the compiler does not match the unlabeled trailing closure to that parameter. Instead, it examines the next parameter to see if that should be matched to the unlabeled trailing closure, as in the following example: |
74 | 84 |
|
75 | 85 | ```swift
|
76 | 86 | func showAlert(
|
77 | 87 | message: String,
|
78 | 88 | onPresentation: (() -> Void)? = nil,
|
79 | 89 | onDismissal: () -> Void
|
80 |
| -) {} |
| 90 | +) { /* ... */ } |
81 | 91 |
|
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. |
| 92 | +// `onPresentation` does not require an argument, but `onDismissal` does, and |
| 93 | +// there is no subsequent trailing closure labeled `onDismissal`. |
| 94 | +// Therefore, the unlabeled trailing closure is matched to `onDismissal`. |
85 | 95 | showAlert(message: "Hello, World!") {
|
86 |
| - // On dismissal action |
| 96 | + // On dismissal action. |
87 | 97 | }
|
88 | 98 |
|
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`). |
| 99 | +// Although `onPresentation` does not require an argument, there are no |
| 100 | +// subsequent parameters that require an argument before the parameter whose |
| 101 | +// label matches the next trailing closure (`onDismissal`). |
| 102 | +// Therefore, the unlabeled trailing closure is matched to `onPresentation`. |
93 | 103 | showAlert(message: "Hello, World!") {
|
94 |
| - // On presentation action |
| 104 | + // On presentation action. |
95 | 105 | } onDismissal: {
|
96 |
| - // On dismissal action |
| 106 | + // On dismissal action. |
97 | 107 | }
|
98 | 108 | ```
|
99 | 109 |
|
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 | 110 | 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