|
| 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