SE-0279: Multiple Trailing Closures [Amended]

+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

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