SE-0279: Multiple Trailing Closures [Amended]

What is your evaluation of the proposal?

-1.

The premise of the proposal is a subjective preference for expressing multiple closure arguments in some form of trailing closure syntaxes. The core rationale, as I understand it, is the author's opinion how the use of trailing closure in a call site with multiple closure arguments causes confusions and legibility issues, due to the unsettling asymmetry.

While I do not entirely disagree, the proposal does not explain:

  1. Why should using multiple trailing closures be the language's terse solution for function calling with multiple closure arguments? Do we need a terse syntax?

  2. What are the pros and cons, regarding e.g. expressivity and information hierarchy, in introducing new grammar and syntaxes into the language?

    Taking the prime example in the proposal & the discussions, today's language and tools can sufficiently express and format it in today's syntaxes:

    UIView.animate(
        withDuration: 0.3,
        animate: {
            self.alpha = 0.0
        },
        completion: { _ in
            self.removeFromSuperview()
        }
    )
    
    Binding(
        get: { self.entity.isEnabled },
        set: { self.entity.isEnabled = $0 }
    )
    

    In my opinion, this already has a well-defined and natural information hierarchy, and carrying the same amount of information as the proposed syntax. So it is very hard for me to believe that the language needs new syntaxes, even if we put aside the controversies around the bundled rules.

  3. Quoting the proposal:

    As a result, if we ever need to append an additional closure argument to a function, many of us find ourselves having to rejigger our code more than may seem necessary [...]

    What is the thinking behind the issue that it must be resolved by expanding the Swift language grammar with an form of "chained labelled curly brackets" calling syntax, IMO very unfamiliar in C/Java family, or even among Swift-like languages with closure/lambda support?

    Is it impractical to trackle this as a refactoring problem in IDEs, formatter or linter?

Is the problem being addressed significant enough to warrant a change to Swift?
Does this proposal fit well with the feel and direction of Swift?

Unconvinced.

14 Likes

Agreed. I would prefer a full holistic change, even if it is source incompatible or no change at all.

3 Likes

I don't understand what problem is being addressed by this holistic “solution.” Swift today supports passing multiple closures and using trailing closure syntax. Only one closure can be “trailing;” it's the one at the end of the argument list. The other ones don't trail, so they don't use trailing closure syntax. This is by design, at least for some of us who were involved in the design.

I'd like to elaborate just a little more on why the current design is good and why the proposal is bad for the language, then I'll be done here.

Trailing closure syntax leverages “code shape recognition” instincts formed by the precedent of common built-in constructs like for loops, where the body, which is part of the loop, “hangs off the end,” of the construct rather than being nested inside it. That position at the end is a recognizable, uncluttered, place of privilege; it signals to the reader that it's the main part, and if there are braces in other parts of the construct, they tend to get nested inside parentheses:

for x in (a.lazy.map { $0.aspect }) {
  theBody()
}

(a.lazy.map { $0.aspect }).forEach {
  theBody()
}

Extensible constructs like if/else and do/catch, with multiple braced parts, are recognized as a chain of these little patterns, rather than as one big unit, so are analogous to chained method calls:

if_(someCondition) { 
  clause1()
}
.elseif_(nextCondition) {
  clause2()
}
.else_ {
  clause3()
}

Sometimes the conditions/patterns in these constructs involve secondary expressions with braces. As with for, these secondary expressions don't move to the end of the unit just because they involve braces:

if (a.contains { $0 < 0 }) { 
  clause1()
}
else if (a.contains { $0 % 2 == 0 }) {
  clause2()
}
else {
  clause3()
}

The one exception to this rule is repeat/while, which is extremely rare in practice and in my experience reads very poorly when the condition involves a closure, because it clashes with expectations set by the rest of the language.

These proposals suggest that the main/body part of function calls that take multiple closures should no longer be at the end, but somewhere in the middle. There are at least two problems with that:

  • That position is inconsistent with the precedent set by language features, which will make it harder to recognize patterns in the code.
  • Independently of any precedent, that position, “buried” in the interior of the function call expression, with a label and another closure following it, is objectively less prominent.
21 Likes

I think, this would be the best solution and it could be done without breaking sources, if the label for the first trailing closure was optional at the beginning. The compiler could spit out a warning, that in the future the label will be mandatory, which could be introduced in Swift 6 in some special syntax mode.

Whoa! I fell asleep, and woke up to find this proposal headed in a source breaking direction.

Unless I am completely misunderstanding, the benefit to be derived from adopting the proposal is in the nature of improved formatting. Correct?

If so, I have a very difficult time balancing that benefit against the major downside of breaking source compatibility. What am I missing?

Apologies in advance for making such an ill-informed post. I’d like to think that, in this case, distance from the details provides useful perspective.

1 Like

Rule 3 clearly states a source-breaking change from the current implementation. What do you propose to avoid that @zollerboy1?

In my opinion, the best approach is to accept this proposal in the current version of Swift and address the inconsistencies by implementing rule 3 in Swift 6.

1 Like

As I said, I would propose to amend rule 3 before Swift 6 as follows:

AIUI, rule 3 is only source breaking, because it says, that even the first label must be specified. If there are other reasons which I don't see ATM, please enlighten me.
Otherwise it would be not source breaking, if we said that until Swift 6 the first label is not mandatory but instead the compiler warns the user, that it will be mandatory in a future language version.

Basically I'm proposing, that we leave rule 3 out, but inform the user, that it will exist in Swift 6.

1 Like

Negative.

Trailing closures is a great syntactic tool and when used right, makes the call site read fluent and natural.

The proposal correctly points out a few problems when passing multiple closures in a single function call. Especially when these have a natural order given by execution time, which do not match the natural order given by importance. E.g, a completion handler naturally follows an animation block, but the animation block is the primary action. Or in SwiftUI, where the a child view and an action block are both competing for the attention of the reader.

However, I think this proposal tries to solve the problem using the wrong tools: I don't think this warrants a change to Swift itself, but rather warrants a change to our tooling, notably to Xcode. The proposal correctly states that trailing closure syntax is indeed popular, but I think wrongly attributes this popularity in toto to its utility. I think its popularity is also in part due to Xcode's aggressive use of it in code completion.

I think Xcode should only offer this syntactic rewrite for function calls that only pass a single (trailing) closure as an argument. I also think that using trailing closures for the last of multiple arguments in function calls, should produce a warning, and that Xcode should provide a so-called fixit to roll the closure back into function invocation parens.

At call sites, it would have almost no discernible changes from this proposal, except for a few additional commas, and moving the closing parenthesis to the very end. Users (and indeed Xcode) could still format the code using almost identical line breaks and white space.

But the benefit would be that this would require no new syntax, making paring easier for both humans and computers alike.

I don't think so. I do, however, think that it is significant enough to warrant a change to the tooling.

No, I don't think so. I think it adds complexity and confusion.

I've never used or seen anything like it.

I've followed the discussion, even to the point of getting notifications for each new post, read the proposal and followed the previous review round as well.

15 Likes

So essentially you propose deprecating the omission of a specified label in the first closure in a function call. I still consider this much too aggressive. I don’t think it’s right to deprecate syntax that’s currently valid in the same language version. It’s my opinion to share that changing the syntax of such a popular feature should require a less aggressive strategy.

  • What is your evaluation of the proposal?

-1, though better than the previous proposal.

This does not help readability for me. I agree with others that have said trailing closures are great when you have just one closure. With more than that it becomes hard to parse where the function even ends, and we have issues like "should newlines be allowed", etc...

@dabrahams said this all way better than I can.

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

No, the current non-trailing syntax using parenthesis is sufficient and clear.

The tooling should be updated to avoid suggesting trailing closures in cases where there are multiple closures. Fixits should be offered for current code like UIView.animate(withDuration:animations:completion:).

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

Maybe. No strong opinion

  • 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've read through almost all the posts in both proposal topics.

5 Likes

This is perfect

2 Likes
  • What is your evaluation of the proposal?

I’m +1 on this one. It’s not perfect and with the current syntax it’s hard to. I’m positive though that in Swift 6 better first-closure label rules will be established.

Additionally, with the rising popularity of DSLs in SwiftUI and such frameworks, I believe that the proposal would really help making code cleaner and more readable.

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

Definitely, readability is at the core concept in Swift. Additionally, this proposal takes a step in the right direction, by addressing some inconsistencies in the language and making it easier to learn and more appealing.

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

Yes it does. I was not in favor of the initial proposal, but the syntax in the amended one really feels like Swift code.

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

No, I haven’t.

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

I have thoroughly followed and been involved in the discussion. I have carefully read the proposal and I have also taken into account community member ideas.

  • What is your evaluation of the proposal?

Enthusiastic +1 with some suggestions below.

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

Yes. Here's why:

Trailing closures look good and @dabrahams described it well. 'Trailing closure syntax leverages "code shape recognition" instincts formed by [some precedents]'.

But more than that, I argue that adjacent braces and parentheses, or braces within parentheses, are generally confusing and appear out-of-place.

It's somewhat true when inline: foo({ ... }), but more clearly true for this:

foo({
    ....
})

Braces indicate structure, parentheses grouping, but here it looks like parentheses are also getting in on the structure game. Even moreso this, with the straggling close parenthesis:

foo(banana: 5, action: {
        ...
    },
    additionally: {
       ...
    }
)

(IMO this is ugly too: var foo = { ... }(), but that's for another day).

Trailing single closures work well to increase visual simplicity, but yes, for functions with multiple closure parameters it's problematic. And it will remain a thorn in the language if something like this proposal isn't adopted.

Also, I don't think it's been a good thing how the best practices for ordering optional parameters are set aside so that the desired closure parameter can take the precious final spot, as others have mentioned.

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

Largely. Earlier versions of the proposal have not, and I would never have used the feature in code I was to write. Only with the modified proposal for this second review have I been on board.

I strongly feel that code like this looks good, and is most consistent with the single trailing closure case:

foo(banana: 5) {
    ...
} additionally: {
    ...
}

I endorse the direction the proposal set as far as best practices for placement and naming of closure arguments.

However, given the valid concerns about cases like Binding(get:, set:), situations of ambiguity, and what seems to be individual style, I think it's better to permit the more awkward looking case where the first closure parameter is also named, and for the choice to belong to the writer of the calling code.

I do think it looks better when the function includes the open-close parentheses foo() action: { ... } as opposed to foo action: { ... } but I think that should be up to the writer, unless there are parsing issues precluding the latter.

I think it might potentially be suitable to have less common cases where the API writer decides this instead using some new attribute, maybe in cases like Binding(get:, set:).

What I don't like is the accumulation of possible trailing closure combinations, some having unfortunate results.

In place of calling code the API writer intends:

foo(banana: false) {
    ...
} additionally: {
    ...
} optionally: {
    ...
}

also permitting this code which lets the wrong closure parameter be un-named seems wrong to me:

foo(banana: false, action: {
    ...
}) {
    ...
}

If considering breaking source compatibility for Swift 6, I think it could make sense if trailing closures were either all or nothing. Meaning the first trailing closure parameter has to be the first of the closure arguments in question, and if it's provided within the parentheses then they all have to be.

Or perhaps instead of making that a hard requirement, have it a clear style recommendation, because I haven't considered all implications of default values.

Regarding the idea:

If a trailing closure argument is declared with a label, it must be specified, regardless of its position (i.e., including the first)

this goes against what we have already and strikes me as fiendish consistency over practicality.

Sacrificing visual cleanliness for simpler rules doesn't seem like a good trade off. I don't think new users will benefit from logical consistency for this, as I think the proposal has a visual consistency that's intuitive. Also it will encourage more parameters without labels which is bad for when not using trailing closure syntax.

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

No

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

Glanced at earlier incarnations of this pitch, proposal, and discussions. Read this latest proposal and this review thread thoroughly.

4 Likes
  • What is your evaluation of the proposal?

-1, for a few reasons, including:

  • Tools should be fixed before changing the language. Xcode doesn't currently handle the completion of multiple closures at all, and suggests trailing closure syntax inconsistently, including after other closures. Fixing these issues, some of which was done as part of implementing this proposal, would go a long way to alleviate the ergonomic issues around multiple closures. Once those changes are in place a real evaluation of changing the language can happen.
  • The proposed solution just doesn't work for many uses of multiple closures, and will have significant issues with existing code. Any code that doesn't have it's primary closure as the first parameter, or doesn't have a "primary" closure at all, will not work well with this design. So while SwiftUI's demos may look slick, I don't believe that will be the case for most APIs. We recently added a second closure parameter to one of Alamofire's APIs. It is actually secondary to the previous trailing closure, but this proposal would complete the two closures with the secondary one as primary, which would lead to poor API usage.
  • The proposal, even if desired, offers library authors no control over the elided first parameter name, and imposes parsing rules on the reader that many users will not be familiar and may take time to learn. For proper integration into the Swift community, this proposal needs to offer customizations points, as it's solution is not one size fits all.
  • Is the problem being addressed significant enough to warrant a change to Swift?

I don't believe so. Even if you accept the proposal's rationale, I don't believe it made the case that the problem described is severe enough to justify such a large change to the language. The tooling should be fixed and the problem reexamined.

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

I don't believe so. Swift already has a reputation for arbitrary changes to language syntax in the name of sugar rather than tackling the real missing features and this proposal will just reinforce that idea. Given that we can get most of the benefit of this proposal by just improving the tooling, that approach should be explored before adding yet more syntax rules to the language.

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

I don't think I've used other languages with a feature like this.

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

In depth read of both the current and previous proposals and participation in both review threads.

18 Likes

I second this. IMO closures inside of the argument list just don't look very good to the point where it's actually confusing.
For me this alone justifies a language change, but many other people seem to not have a problem with this, so obviously it is very subjective. Or are those who give this proposal a -1 also bothered by braces inside of parentheses, but it doesn't suffice for them to change the language syntax?

1 Like

Personally, the only case where I am somewhat bothered by it is when I call a function that takes a single, unlabelled closure argument, and I can't use a trailing closures because I'm in the condition of an if or similar:

if someFunc({ somethingElse() }) {
    // ...
}

That one I find slightly more unpleasant if I want multiple statements in the closure without breaking it out into a local function or closure variable:

if someFunc({
    somethingElse()
    evenMore(foo)
}) {
    // ...
}

The second one might be an artefact of my formatting style though, because if there was a label I would probably format it like this, which (somehow) I don't find as egregious:

if someFunc(
    doStuff: {
        somethingElse()
        evenMore(foo)
    }
) {
    // ...
}
4 Likes

Why not just remove trailing closures altogether and encourage people to use syntax that matches the declaration site. We're saving at minimum of typing () and at most () and a label. Now we are wondering if we should type all the other labels (somewhere else) and just put it all outside of the parentheses. People are wondering if the OTHER arguments should have a chance to be placed outside as well 'trailing'. I understand this will be an extremely unpopular opinion, but this syntax feature was never really warranted to begin with. It saved us from typing two characters. Every proposed solution so far to allow for multiple trailing closures, is actually really confusing to read and messy. It also doesn't self document very well because the declaration site for functions is looking less and less like the call site. The more I read all this the more and more I've thought "let's just remove trailing closures as it never solved an actual problem to being with". It was always simply alternative syntax that encouraged two ways of doing the same thing. They are not functionally different, don't make anything actually easier, and create this sort of existentialistic question about the nature of arguments and training arguments to argument list.

The extreme proposal is to say: How about allow all argument to trail? I mean we could easily get the parser to do that. But it servers no purpose. Trailing closures also never really served a purpose. I don't use them because they don't self document.

5 Likes

Could you explain what makes the proposed solutions you see “messy“. I agree that some are indeed messy, but some in the original proposal seem pretty clean and readable to me. One example of that would be:

UIView.animate(withDuration: 0.3) {
    ...
} completion: {
    ...
}

Yea, simple, you could easily just write

UIView.animate(withDuration: 0.3, animations: {} , completion {}).

In your example you saved typing the "animations:" label and having to put the closing paren at the end.

I think it only goes to demonstrate trailing closures solve absolutely nothing except typing a label, and with the notion that we could have multiple trailing closures, it makes me realize how silly the syntax was to begin with is. One trailing closure is enough. I can get it. The rest should just be put inside the parameter if you need more.

Why not allow this

UIView.animate 0.3, {} , completion: {}

or

UIView.animate 0.3, {} , {}

or

UIView.animate() 0.3, {} , {}

or

UIView.animate() withDuration: 0.3, animations: {} , completion {}

By analogy the above should be permissible as well if we allow multiple trailing closures with labels. Someone will realize that we could take it a step further.

5 Likes

Don’t you think it would really benefit DSLs?Imagine SwiftUI code filled with extra parenthesis, commas and unnecessary labels.

That is, writing a SwiftUI Section without parenthesis clears up the code and draws visual emphasis on the important elements of the code - the closures. Without trailing closures, the code seems like a simple initialization, whereas DSLs are specifically designed to serve a purpose and focusing on their elements is rudimentary.

3 Likes