SE-0279: Multiple Trailing Closures

It cannot be the former because that argument would not then be a closure expression. The same issue applies to a .wibble() following an existing single trailing closure.

1 Like

Neither the current single trailing closure expressions nor dropping the outer braces from the proposed syntax here would inhibit extensions on functions.

There are a number of existing ways to pass a function as an argument—map(+), map(\.isEmpty)—which cannot be written in trailing position as-is because they are not closure expressions, but all can be trivially written in that position when wrapped in a pair of braces—map { $0.isEmpty }.

The same would apply to your wibble or toResult: an extra pair of braces would do the trick when/if they become possible. To prophylactically surround all trailing closures with an additional pair of braces is an alternative, but I cannot see why it would be preferred when the stated purpose is syntactic sugar.

Edit: If it were to be your opinion that some sort of delimiter is necessary between a wibble applied to an argument versus the whole function call—isn’t that an argument for the existing parens; that is to say, not using any sort of trailing syntax? In other words, aren’t we just swapping the shape of braces here?

3 Likes

More thoughts...

Perhaps an improvement to the current proposal would be to add a restriction that trailing closure group, when present, must include all the trailing closure parameters. This way the caller cannot split the arguments at an arbitrary point within the couple of trailing closures parameters the function may have, increasing consistency between the call sites.

So this should be made illegal with the new syntax:

UIView.animate(withDuration: 0.4 animations: { frame.size.width += 10 }) {
  completion: { _ in backgroundColor = .yellow }
}
// error: `animations` must be included in the trailing closure group
// fixit:
UIView.animate(withDuration: 0.4) {
  animations: { frame.size.width += 10 }
  completion: { _ in backgroundColor = .yellow }
}

We could also make it a warning to use the existing trailing closure syntax when there are two or more trailing closures:

UIView.animate(withDuration: 0.4 animations: { frame.size.width += 10 }) { _ in
  backgroundColor = .yellow
}
// warning: `animations` should be included in a trailing closure group
// fixit:
UIView.animate(withDuration: 0.4) {
  animations: { frame.size.width += 10 }
  completion: { _ in backgroundColor = .yellow }
}

The all-in-parenthesis syntax would still be allowed with no error or warning though:

UIView.animate(withDuration: 0.4 animations: { frame.size.width += 10 }, completion: { _ in backgroundColor = .yellow })

This would address one of my concern that there are too many syntaxes to write the same function call. There would be a maximum of two possible syntaxes for a function call where the compiler would not complain. By limiting the possible options you make the code more predictable and easier to read.

How does this work with autoclosures?
It is a closure, but touches and feels like variables.

I think these rules are reasonable.

Wait, you’d want to deprecate existing syntax, breaking source for those who use warnings as errors?! I certainly disagree that this would be reasonable. The bar for breaking source is active harm.

3 Likes

I don't think new warnings is considered breaking source. Warnings as errors is a choice users make which may occasionally require source changes that would not otherwise be necessary.

7 Likes

Possibly idiotic thought, and I'm coding here without the benefit of a Swift IDE, but if you are willing to have this as your 'multiple trailing closure' syntax...

tryToDoIt(foo)
    .success { ... }
    .failure { ... }

This can be done by having tryToDoIt(...) return something that implements success and failure as methods that take a single closure parameter and also return that something.

Obviously implementing this something all over the place would be tedious, but perhaps there is a language feature that would make it less painful where nescessary? It feels a bit/very superficially like promises.

1 Like

+1 because I know this is for swiftUI.

Can you drop the outer braces?

when(isValid)
then: {}
else {}

Is that too much for the parser?

Do you know of any example where uses of officially supported Swift syntax have been deprecated with warnings without evidence of active harm since Swift 3?

This feature would have no effect at all when the list of closure parameters is intermitted by something else, wouldn't it? (example: func closure(block: () -> Void, times: Int, block second: () -> Void)).

So it would probably encourage people to not order their parameters in a way that makes the most sense, but rather to move all closure-type parameters to the end of the list.
I don't like that prospect, and afaics, there is no good solution to lift this restriction.

I don't think this is idiotic. This is a very reasonable way to design APIs, and one that already works today.

6 Likes

It's not idiotic at all. My only concern with this kind of design is that it might mean you have to expose intermediate values as API for the result of tryToDoIt(foo) and .success { ... } that you wouldn't have to if they could be expressed atomically all as arguments to one function invocation. Sometimes, that's appropriate.

1 Like

Chaining syntax would be a pretty cumbersome way to express an API that requires exactly two closures with a specific signature, especially if a computation needs to be performed and a result returned after the last closure parameter was provided. It works very well in some contexts but is not always an alternative to closure arguments.

3 Likes
  • What is your evaluation of the proposal?

I like it very much as it improves the readability by introducing a very nice symmetry and structure: normal parameters live within parentheses and closure parameters live within braces while commas can be elided.

  • Is the problem being addressed significant enough to warrant a change to Swift?

Yes, definitely as calls to methods with multiple closures can be written much more readable and structured.

  • Does this proposal fit well with the feel and direction of Swift?

Yes, definitely, because methods with multiple closures are a common pattern in Swift.

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

Smalltalk comes to mind which does not have this problem, because it has no comma separated parameter list enclosed by parentheses but just labels with colons.
The proposal comes as close as possible while keeping the style of Swift‘s syntax.

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

I followed the thread and did a quick reading of the proposal.

-1

Another special syntax unique to swift with no real substantial benefit over existing syntax - paid for with extra learning curve and reduced usability for people who work in multiple c-style-syntax languages.

Not worth it.

5 Likes

Agreed -- chaining is a valid API design, but it has tradeoffs that aren't always desirable. Parameters passed via chaining must be optional unless you want to create a complex arrangement of intermediate types, and it's much less discoverable vs seeing everything in a parameter list. As was mentioned, it can be difficult to know when the caller has finished configuring an object with chaining. It can also be significantly more burdensome on the API author to create a chaining-style API vs passing in simple parameters.

I also wanted to voice some opposition to the suggested syntax without the braces. It would be an appropriate choice if Swift were a scope-by-indentation language like Python, but it doesn't fit into a brace-scoped syntax. As far as I'm aware, nothing but switch cases create a scope without needing braces, and I think they're necessary to make this feature readable.

2 Likes

Just having some fun rewriting the proposal using a switch-like syntax:

UIView.animate(withDuration: 0.7) {
animations:
    frame.size.width += 10
completion: _ in
    backgroundColor = .yellow
}

It actually looks closer to the current trailing closure syntax in that you don't have two levels of braces. Visually it's like one big closure with two different code paths. There's no way to set the completion block to nil in the example above however.

Has this been considered before? I especially like that you don't end up with too many braces.

4 Likes

I think the closure arguments would be quite hard to parse. Esp. the automatic ones.

Do you mean hard to parse for the human eye?