SE-0279: Multiple Trailing Closures [Amended]

Personally, I’m in favor of the amended version of the proposal. I would, though, like to see the ability to optionally label the first trailing closure as in some APIs the purpose of a closure is unclear without a label. I understand that my request is source-breaking, but I think its addition to Swift 6 would be welcome and would make Swift more consistent with its closure behavior.

As some members of this community have said, the current proposal would be difficult for a beginner to grasp and I think the language has the opposite goal in mind; being easy to understand and intuitive even to beginners.

What I recommend is that the closure behavior be made more consistent in Swift 6 and that trailing closures be better explained in the documentation.

3 Likes

What is your evaluation of the proposal?

Pretty firm -1

This adds a significant amount of syntactic complexity, for very little gain. I don't agree with the core team that this is a problem worth solving - to paraphrase what others have said, trailing closures are a convenience, not a right.

Not every API needs to use trailing closures - they're cool, but this proposal stretches them so far it ruins them. It removes all of the convenience, and actually adds more cognitive overhead by making all Swift callsites less easy to parse (for a human) and understand.

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

I don't think so, and I think the proposal overstates the benefits it could bring to APIs such as UIView's animation methods. I don't think Combine's sink API should accept two closures, either - a single closure with an enum parameter would be a better model for success/failure conditions (hello Result!), and I'd say that even if we had multiple trailing closures.

It's also critical to consider what regular users would think of this. I saw a thread on the r/swift subreddit the other day about how we can now use keypath literals in place of closures, and one user had this to say :

There's enough added to Swift already that it's just another set of visually clumsy code, but now there is another way to make something and it's supposed to be less visually clumsy, but isn't really.

I see those kinds of concerns more or more often outside of these forums, and with each release adding yet more alternative syntax, it becomes harder to say they're wrong. Why do we even have high-level programming languages? Not for the benefit of the computer - it doesn't care how you wrote your code; it's for the benefit of the human beings reading and writing that code.

A language which is unreasonably complex to learn and understand is, almost by definition, a poorly-designed language. Some features, of course, come with unavoidable complexity. Function calls involving closures are not one of those features - this is entirely avoidable complexity, and the use-cases are not compelling enough to overrule that IMHO.

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

I don't think so. This new syntax has none of the convenience of trailing closures, and more complexity than a regular function call. It makes the language more confusing to newcomers.

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

N/A

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

Participated in original pitch and review, read the changes and responses here.

19 Likes

On the subject of "primariness", I think it's worth considering completion handlers in their own category. I would guess that at least 80% (and probably more) of the APIs people use which accept more than one closure parameter actually take a closure and a completion handler. The general problem is that closures (trailing or otherwise) suck for completion handlers, and every way of shuffling them around exposes one inadequacy or another in using them that way.

As things are today, using closures as completion handlers results in the well-known "pyramid of doom" effect, where we use indentation to clearly mark "this set of things happen later", with a bunch of closing punctuation following at the end.

UIView.animateWithDuration(..., 
  animations: {
    // Animation 1
  }) {
    UIView.animateWithDuration(...,
      animations: {
        // Animation 2
      }) {
        UIView.animateWithDuration(...,
          animations: {
            // Animation 3.
          }) {
            // Finished.
        }
    }
}

However, as you note, it's not really clear that the completion handler should occupy this "primary" position in an animation API, allowing it to be tacked on the end without a label. It isn't obvious by looking at the code what that trailing closure even does (maybe it takes a bunch of animations to run in parallel - who knows?).

Unfortunately, your solution of moving the completion handler elsewhere in order to force it to have a label (and allowing the more obvious animation block to omit a label) suffers a different problem:

UIView.animate(
  withDuration: 0.3, 
  completion: { _ in 
    UIView.animate(
      withDuration: 0.3, 
      completion: { _ in self.view.removeFromSuperview() } // Action 3
    ) { self.view.alpha = 0 } // Action 2
  }
) {
  self.view.center = newLocation // Action 1
}

You actually need to read this block from the bottom up in order to get a picture of which events happen in which order, which is clearly not ideal for readability.

For completion handlers in particular (which, I repeat, are almost certainly the most popular use of multiple closures today) we have a much better solution - one which eliminates the pyramid of doom, makes it obvious what the completion handler is, when it is being run, and stops those it hogging the "primary" position in so many functions. It is a feature that essentially every other modern language already has - async/await:

await UIView.animate(withDuration: 0.3) { self.view.center = newLocation } // Action 1
await UIView.animate(withDuration: 0.3) { self.view.alpha = 0 } // Action 2 
self.view.removeFromSuperview() // Action 3

Really, I wish we'd stop talking about the UIView.animate API in connection with this change. This proposal will do absolutely nothing to make those callsites any cleaner or clearer than they are today. As you put, it makes one feel as though the proposal authors do not have a good grasp of the problems they are purportedly trying to solve.

24 Likes
  • What is your evaluation of the proposal?

I'm going to echo others feeling on this. As it stands I would prefer when multiple trailing closures exist, all argument labels should be present. Only when there is a singular trailing closure should the label be omitted.

With the above change I would be a +1 on this proposal. In its current state I would be +0.5

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

When looking at small bitesized examples it seems like a silly and almost pointless change. But with this allowing for more broad API changes to increase logical layout of function call-sites (see the Combine example), I can see this change really improving the natural flow of the code.

With the above suggested change around adding all of the argument labels for multiple trailing closures, yes, this does warrant a change.

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

With the above pre-requisites, yes.

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

N/A

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

I followed the previous discussions, this thread and the proposal.

+1

I like the proposal for its simplicity, which can be summed up as:

The existing syntax for single trailing closures remains the same, but now you gain the ability to add additional, labeled trailing closures.

I like @kylemacomber's description of an API design choice that has often bothered me:

For those that argue that trailing closure syntax should be treated as only a convenience, this should be especially irksome: we are forced to abandon an intuitive and otherwise-recommended ordering of parameters in order to accommodate the possibility of a syntactic convenience. In other words, we have to choose between optimizing for trailing closures, or not, and given their popularity we are generally strongly encouraged to optimize for them. The proposed syntax simplifies API design in many cases by eliminating the need to make this choice, since the recommended ordering would be the same.

Separately, I sympathize with the arguments in favor of having the option to label the first trailing closure. However, if that is considered a problem, then it is a problem that exists in the language today, and is probably best addressed by a separate proposal. This proposal does not place any additional burden on supporting leading trailing closure labels in the future, since whatever solution we come up with to handle the source compatibility issues related to that for single trailing closures would also apply to expressions with multiple trailing closures.

However, I don't agree with arguments that labeling the first trailing closure is necessary for clarity at the call site. Most existing control flow syntax seems to contradict this claim, e.g. we don't include a then on the end of if and while expressions. For similar reasons, an animations: label feels superfluous following a function called animate(). It seems like a critical guideline for using trailing closures from the beginning has been that the name of the function should contextualize the unlabeled trailing closure, and this proposal stays consistent with that.

Yes, I believe it is.

I do sympathize with the arguments that Swift is complex and this proposal is making it more complex, especially for beginners. The language would certainly be simpler (putting aside whether it would be better) if trailing closures did not exist at all. However, given that they do exist, and there will likely never be a consensus for removing them, I think it's worth considering improvements.

With regard to beginners, not all complexity is the same. Every language has weird quirks that, once learned, cease being roadblocks ("learn once, never forget"). A commonly-cited example is square brackets in Objective-C. I'm skeptical that the proposed syntax would be a long-term impediment to beginners, since they already must learn very similar-looking syntax with chained if/else if and do/catch expressions. I have yet to discover or see presented a realistic example using the proposed syntax that feels harshly confusing.

Given that, and the improvements I feel that the proposal would bring to API design mentioned above and in the proposal, I believe the change is warranted.

Yes, as stated above, I believe it's a natural extension to the existing trailing closure syntax.

n/a

An in-depth study, considering the current proposal as well as previous and alternative proposals, and reading all previous review comments.

2 Likes
  • Single closure : label can be omitted optional

  • Multiple closures : labels must be added include _ required

1 Like
  • What is your evaluation of the proposal?

    Very heavy -π [-1 for seachability].

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

    I think not. In my mind, the problem already has a solution. Just put all your closures in the parameter list with the rest of your arguments. It's not really a concern of character count (we aren't code-golfing for the most part), it's just an additional comma for each closure to separate the method's arguments. You get the parameter labels, so there is no confusion about what the closures do (unless you have bad API design, but that's a different discussion), and you get the closing parenthesis after the final closure argument to keep everything nicely grouped together and easy for the eye to parse (granted, that's probably an opinion of mine and will be debated by some).

    To be perfectly honest, I would prefer completely removing trailing-closure syntax from Swift rather than have this. It would help alleviate the issues that can occur with bad API design. I've personally found myself confused by the result of trailing closures at times, especially in initializers or when the closure is the only argument of a method (or combining the two :flushed:):

    let handler = EventHandler { event in
        print(event)
    }
    
  • Does this proposal fit well with the feel and direction of Swift?

    I actually need to answer this point in two parts:

    • Feel: No. What this gives us is a floating identifier followed by a colon after a trailing closure. Does that identifier have a keyword before it declaring what the identifier is for? No. Is there a type definition after the colon? No. Is it a scope label? Yeah, that's probably the closest thing. So now I can break out of the closure? Sorry, nope, that's not what this is. Being able to overload the . operator would make just as much sense to me.

    • Direction. Sadly, yes. Since the dawn of the SwiftUI era (and don't get me wrong, I love SwiftUI), we have seen a plethora of proposals adding syntax sugar that causes more confusion than conciseness. Granted, I think property wrappers are amazing (if used correctly), but some like function builders and the optionality of return statements are incredibly finicky and make it really easy to create code the is hard to follow. The funny part is, I think that the SwifyUI example in the proposal is actually the hardest one to parse, and once you add that in with the vast amounts of nesting in brackets that it can take to create a view, you I won't be able to see the program for the code.
      Anyway, the point is that this is another proposal that follows that pattern of trying to add sugar to something that is plenty sweet already. Add some salt for once.

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

    N/A. I have not seen similar features in other languages I have worked with.

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

    I've loosely followed the proposal on the forums and read through the proposal while I was writing this response.

15 Likes

Actually, I think UIView.animate is one of the few APIs that would immediately benefit from this proposal, due to its first closure being its primary action. Of course, most other APIs will not get that benefit and will have a poor interaction with this feature by default. And like you say, the bigger improvement would be to make completion handlers unnecessary.

1 Like

-1

No substantial improvement but increases complexity.

5 Likes

This feature would allow calls to UIView.animate to be written slightly differently, sure - it's introducing a different syntax, after all. I wouldn't say that this alternative provides any "benefit", though.

There are certainly issues with the existing API, but this feature doesn't make them any better. It doesn't make the pyramid of doom any less prevalent or easier to read, it just adds more labels in the pyramid.

Another thing: I posited that completion handlers make up the vast majority of uses of multiple closures. I don't have data to back that up, unfortunately (I'm not sure how I would mine that information from GitHub), but actually, the fact that async/await even exists kind of supports the argument by itself. Why would so many languages add special syntax to flatten those "continuation" closures if it wasn't an extremely common pattern?

A lot of the arguments for this proposal seem to be based on assertions without evidence/intuition. If this was the 1970s, that would be fine, but this is 2020 and these days we make decisions based on data. I have yet to see data indicating that this is even a problem, let alone that developers prefer this syntax, and I think this shows a lack of care and consideration when compared to other aspects of the language.

It seems like these days, Swift is a toy to allow Apple to experiment with exotic syntax and rush it in to production. I think the core team, and in general the users on these forums (who are some of the most knowledgeable Swift developers in the world), are underestimating the impact this additional complexity will have for everybody who is not us.

11 Likes

+0.5

For me trailing closures were always among the best looking and most compelling syntactic features Swift had to offer. They fit the language because they are IMHO straightforward and easy to understand.
Therefore I think that it is a no-brainer, to extend this syntactic sugar, where it's usability is limited. I agree with the arguments presented in the proposal that this is e.g. the case, when there are multiple (possibly defaulted) closure parameters at the end of a function.

I think that the proposed syntax builds really well on top of our current trailing closures and that the simplicity is perfectly maintained, at least in the presented examples.

As many others also mentioned in this thread, I can think of some use cases though, where the API design would really benefit from the option to explicitly state the argument name of the first trailing closure too. This would be especially useful for functions, which take multiple equally important closures. Binding(get:set:) mentioned by @Lantua is probably the most prominent member of this group of functions.
APIs designers should even get the possibility to obligate the explicit first trailing closure argument name IMHO, because I believe as many others, that the caller of the API should not necessarily have to decide for or against it by himself.
Something like an opt-in function annotation like @michelf suggested here should be considered (obviously the best approach would be to require the argument name, if the label wasn't underscored in the function declaration, unfortunately that would be source breaking).

I would definitely be +1 for this proposal, if it became possible, to label all trailing closures and to require such use in an API. It would also make the Renaming Trailing-closure Functions in the Standard Library pitch somewhat obsolete, which I would rather appreciate.

In my eyes yes, especially if labelling all trailing closures became a thing.

Definitely. IMHO it extends the current trailing closure syntax very naturally and also follows the current direction of Swift, by allowing e.g. better DSL-APIs.

N/A

I read the proposal and the whole second review thread.

2 Likes

I found your choice of () in the second example interesting. Using an optional first argument label could also prefer this parenthesized style to avoid the juxtaposition issue mentioned in the proposal.

1 Like

To be honest this syntax:

myArray.filter() where: { ... }

was unintentional. But as you mentioned there are some benefits. The parenthesis differentiate between properties and methods, which I think is quite good for a beginner.

Of course, this syntax would add more visual clutter and create inconsistencies between a no-label and a labeled first-closure approach. Also, to be fair there is (currently) no feature in swift that allows for labels or keywords to be next to properties (except for operators). As such, these are clearly method calls (after getting used to the following syntax):

foo.bar { ... } 

myArray.filter where: { ... }
3 Likes

What's good for beginners is good for everyone. Functions, methods and initializers should IMO always have parentheses for clarity. Omitting them should be reserved for the rare cases where the function acts like a language keyword.

9 Likes

I agree. Additionally, any reasonable editor would color or style labels differently to make it absolutely clear. A newline could also help make the juxtaposition clear (particularly in DSLs). The proposal seems to dismiss this because in their opinion the juxtaposition is not clear enough or jarring due to lack of special characters. I think the ability to either add () or ability to drop the label should provide alternatives if juxtaposition is not clear enough or jarring in some contexts. With an async syntax potentially coming out that will make completion handlers less common, I think multiple trailing closures will mostly be used in DSL contexts. I think it would be nice to allow as much flexibility as possible here. Particularly since allowing a first closure label is more consistent with normal arguments. I hope this feature ends up coming in later if the proposal is adopted without them.

I do think filter is kind of a bad example though. It is common enough that I don't think it needs a really clear label. I think an exception for normal verbosity is fine for a handful of functional programming inspired functions. We should just know all of these by heart. However, I think the ability to add an optional label is great for those learning the language.

1 Like

Many of my thoughts are written out in more detail in Renaming Trailing-closure Functions in the Standard Library - #7 by allevato and later in that thread, and I've tried to summarize them below.

I still feel strongly that the syntax in the current revision of the proposal is a vast improvement over the original version. However, my advocacy for this solution in the first review was also predicated on the idea that it would allow the first (or only) trailing closure's label to be written out, which would remove the syntactic ambiguity when using a function call with a trailing closure in a conditional statement. However, that wasn't done here; by omitting that possibility, the syntax misses out on a possible solid improvement to the language.

The issue is simply this: Swift's trailing closure syntax was designed with single trailing closures in mind. If we want API authors to now write functions that use multiple closures and use trailing closure syntax, we need a holistic solution that unifies trailing closure syntax everywhere in the language. Grafting new bits of syntax to the existing system with its pre-existing exceptions is just going to make the language more complex and confusing.

If I could design what I think are ideal rules for trailing closures from the ground up, they'd be something like this:

  • All closure arguments in a function call which are not followed by a non-closure argument can be written using trailing closure syntax. Multiple trailing closures would be expressed through juxtaposition, as proposed here.
  • If there are no arguments preceding the trailing closure(s), the empty parentheses may be omitted (as they can be today).
  • If a trailing closure argument is declared with a label, it must be specified, regardless of its position (i.e., including the first).
  • If a trailing closure argument omits its label in its declaration (uses _), then the first such closure at the call site omits the label entirely; all subsequent closures must be labeled _:.

This would make trailing closure argument labels act almost like all other argument labels. Sadly it doesn't fix all cases of the conditional statement problem—if a function's first closure is unlabeled, it'll still be ambiguous, but in that case, surrounding it by parentheses is probably less ugly than other alternatives I can think of (like allowing _: to be in the first position), and it still solves the problem for labeled closures, of which there are many.

Allowing the first trailing closure to have a label would also fix a major problem for API designers: APIs designed to be used with trailing closures cannot provide any meaningful information through a label about what that closure does. Is it a completion callback? Is it a transformation function? Is it something else entirely? You have to guess from the context, and sometimes it's not obvious. The language's choice to unconditionally elide the label removes the ability of the API designer to make that label meaningful. The thread linked above points this out for a handful of standard library functions that take a single closure argument by proposing to rename the functions to graft the label onto the basename, but IMO that makes those functions look worse, and it does nothing to solve the problem for functions that have arguments preceding the closure, where there is no place you can graft the label onto.

Unfortunately, what I don't have is a great idea of how to balance these concerns with the source compatibility requirement.

Overall, however, I do think this proposal is syntactically a huge improvement over the first iteration, and that this would be a good improvement for emerging multi-closure APIs. But I think some of the details still need to be tweaked. As some folks have mentioned upthread, allowing the first trailing closure's label to be optionally specified (even when there is only one trailing closure) would resolve some of the syntactic ambiguities of conditional statements, and it would give API designers the ability to make that label meaningful again. Then, perhaps a future language mode could start making the first label mandatory as well.

11 Likes

If trailing closures were redesigned from the ground up, I somewhat prefer this syntax for the unlabeled single closure so that it could work in conditional statements. I can't think of any other syntax that feels like argument labels yet would also be non-ambiguous in general cases. Obviously this would require changing how loop labels are specified and break a lot of existing code. I agree with everything else in the if-we-could-start-over design.

myArray.filter: { ... }

+1. This states much more clearly the direction I alluded to here: SE-0279: Multiple Trailing Closures [Amended] - #43 by anandabits. and here SE-0279: Multiple Trailing Closures [Amended] - #48 by anandabits.

I think the alternatives are demonstrably worse. I don't think there's a way to solve this problem well in both the language and standard library without incurring source breakage of one kind or another. So I'm in favor of accepting the source breakage this approach would incur. If the current syntax can be deprecated before it is removed altogether, even better - projects can migrate at their own pace.

7 Likes

We've seen a lot of examples with clarity problems in this thread, but not all of them are related to trailing closures or argument labels, and IMO if we're going to make a change to the language, it's really important to be clear about which problems it solves. As I have said before, the core problem with filter is that we don't know the polarity of the predicate, and seeing where: does nothing whatsoever to help with that. The only cure for making these calls clearer at the call site is to get that information into the name, e.g.

a.selecting { x in x % 2 == 0 } 
12 Likes

I don't think a limitation stemming from the compiler is the right tactic. In previous discussion regarding whether white space rules should be enforced by the compiler or not, most agreed that flexibility from the compiler would be best. I believe the same should apply here. I agree that names of methods with closure arguments should be carefully considered; the example with selecting demonstrates that quite well. Nonetheless, giving API designers the flexibility to optionally add a label is, at least as I see it, more aligned with the design philosophy of swift.

Besides, optionally specifying a label for the first closure would be more consistent with the proposed syntax and would be easier for beginners to grasp. With @allevato's (which is, in my opinion, the most refined up to this point) proposal the only odd thing to a beginner would likely be the fact that the label of the first unlabelled trailing closure (in a function call) can be omitted, whereas other labels can't:

func foo(_ bar: () -> Void, _ baz: () -> Void) { ... }

foo {  // label was omitted
    ... 
} _: { // label can't be omitted
    ...
}

That's a rule I find perfectly logical, as with the lack of _: would make the fact that this is function call unclear.

1 Like