SE-0279: Multiple Trailing Closures [Amended]

As others already pointed out, I don't know of any case when Swift prohibits adding a EOL-comment between two constructs, I believe it's pretty much always allowed to add one and there's no reason to change that.

I agree that it's out of scope, and given the popularity of Swift there will still be developers attempting to write code like this for all sorts of perfectly valid reasons and getting an unexpected compiler error (if the decision is to disallow such code).

One example I can think of is that during debugging I sometimes comment different code blocks to figure out which of them is problematic, and seeing compiler errors while doing that would be quite annoying.

2 Likes

There would need to be multiple similar overloaded trailing closures. Since you would be cutting off the tail, I don't think this would generate any errors or warnings.

However, I think playgrounds would be a place this might be used. Arguably declarative uses like SwiftUI might want to do this too. I didn't think this was important before, but I'm leaning the other way (no limits on comments) now that I'm thinking about use in playgrounds.

-1

This proposal seeks to extend a quite simple feature, which is generally easy to understand and reason about, with a complex extension that seems destined to sow confusion and doubt.

I’d also like to echo @barnard-b’s concern that foo { ... } bar: {...} is one function call, and foo { ... } bar { ... } could be two, differentiated only by a colon. SE-0279: Multiple Trailing Closures [Amended] - #89 by barnard-b

I’m a language lawyer and feature maximalist, but if this feature is adopted with the complex rules currently proposed, I expect I’ll avoid using it.

(That said, in the best-case scenario it seems at least modestly useful, unlike the previous one which didn’t simplify anything.)

In short, no.

Not everything is improved by generalizing. We don’t need to add mixfix operators of arbitrary arity just because we have unary and binary operators. We don’t need to add multiple trailing closures just because we support single trailing closures, where the singularity of them helps make it clear what function call they belong to.

If we could have multiple closures in a way that feels as lightweight and human-readable as the current single-closure syntax, I’d be all for it. But this proposal has made it quite clear that we can’t.

Not really. Free-floating bits of function names don’t have any obvious precedent.

I’m particularly concerned about the idea of not requiring, or even allowing, a label for the first argument. We already went through this with regular parameter lists, and it was found to be confusing and unhelpful in practice, even though using a blank first label is pretty common. I don’t see how this is different.

(If we do go through with this, but allow a label for the first closure, it should of course be possible to use the same syntax for a single-closure invocation)

I read some Smalltalk once

I’ve followed this thread and its ancestors in detail, initially with some optimism.

16 Likes

This discussion makes me think of SE-0046, which established consistent labeling behavior across all function parameters. Prior to that change, the first parameter was given special treatment, implicitly removing the first parameter label.

// before Swift 3.0: declares makePoint(_:, y:)
// after Swift 3.0:  declares makePoint(x:, y:)
func makePoint(x: Int, y: Int)

The change to make all parameters behave the same improved the consistency of the language and supported the "Principle of Least Surprise".

As we now consider support for multiple trailing closures, it seems desirable to consider the same approach. In an ideal world, I think it would make sense to require labels for all trailing closures, unless they are specifically given no external name.

// declaration
func fetchValue(onSuccess: (Int)->Void, onFailure: (Error)->Void) {}

// point of use, labels required
fetchValue 
  onSuccess: { /*...*/ }
  onFailure: { /*...*/ }


//declaration
func filter(_ isIncluded: (Element)->Bool) -> [Element]

// point of use, no label required
filter { /*...*/ }

This behavior is predictable and consistent with other parameter labels. If we were introducing trailing closures from scratch, I think this is the obviously-correct behavior. It adheres to the principle of least surprise; the only principle to learn here is that "trailing closures can be placed outside the parentheses". The obvious downside, of course, is that this simple rule is source-incompatible with existing Swift code, because existing code will often declare an external label for a trailing closure parameter but then call it as a trailing closure without a label.

Assuming we agree that what I've outlined above is the "obviously desired behavior", is there a way to get from here to there? Perhaps we could continue to allow eliding the label in the existing case (single trailing closure), but emit a warning that in the future such code will fail to compile? Is that too aggressive? It's going to warn on a lot of existing code, but other solutions seem to just add special cases everywhere.

I find support for multiple-trailing closures desirable, but I worry about doing it in a way that increases the complexity of learning the language.

33 Likes

I think we could

  1. Make this proposal non-eliding, even on _:
  2. Deprecate-then-remove the single-trailing closure syntax
  3. Introduce first-label eliding on _:

Any approach would require source breaking changes and warrant much more serious consideration than what a single proposal discussion space can provide. Still, the answer to "What would we do on a blank-slate?" is at least probably worth mentioning.

6 Likes

Maybe an optional colon could work in non-ambiguous cases when dropping parentheses too:

if let selected = users.filter: {$0.characters.count == 2}.first {
   print(selected)
}

// or 

ObjectiveC.autoreleasepool: {
    ...
}

I understand the idealistic reasons for this, but it is a small edge case. The compiler should give a good error as written since it would need a newline before bar for it to be two functions. Then there is a 99.9+% chance it would still give a good error since it is equivalent to a hash table collision. I think this is within the error rate of other errors being good errors.

This bothers me too. In part because both new and old syntax is ambiguous in too many contexts. I would like to see change in part because of this. I'm pragmatic enough to leave a few ambiguous cases that would never be encountered following style guidelines, but there are too many right now requiring some horrible explicit cases in the guidelines. Even if not in this proposal, I'd like to at least see a plan to improve ambiguous cases for both new and existing trailing closures.

1 Like

The projects I'm on are using closer to an Allman syntax style than a K&R variant, so we would have each label start on its own line. All comments are always on their own lines before the related code so that also matches our style if describing a closure.

1 Like

This review is probably going to seem very negative. Sorry for that, but I think the proposal is fundamentally flawed and it wouldn't serve the language to pull punches here. -1 on accepting it.

Problems With The Problem

First of all, I find the description of the problem in the proposal to be incoherent. First, it identifies “the” problem with trailing closure syntax as that it only applies to the last closure (i.e. we don't get to omit enough argument labels), and then it goes on to give an analysis where the actual problem is that a label is omitted that was needed for clarity. Making it possible to sugar closures in more argument positions can't help with that.

Second, the description of the problem is uncompelling:

// Without trailing closure:
UIView.animate(withDuration: 0.3, animations: {
  self.view.alpha = 0
}, completion: { _ in
  self.view.removeFromSuperview()
})
// With trailing closure
UIView.animate(withDuration: 0.3, animations: {
  self.view.alpha = 0
}) { _ in
  self.view.removeFromSuperview()
}

In this case, the trailing closure syntax is harder to read : the role of the trailing closure is unclear, the first closure remains nested, and something about the asymmetry is unsettling.

Handling the claims in reverse order,

  • “Something about the asymmetry is unsettling” — just the sort of subjective assertion that makes me deeply suspicious that changes are being driven by criteria that leave us with no identifiable guiding principles. If we start making adjustments to the language whenever “something is unsettling,” different examples and peoples' experience will dictate contradictory choices and we'll be adjusting forever.
  • “The first closure remains nested” — so what? That's not a problem in and of itself.
  • “The role of the trailing closure is unclear” — yes. That's a problem to be solved.

Overall, the readability of the examples is harmed by formatting that hides the structure of the code (and a needless with in the argument label—but never mind that), which makes it hard to evaluate the real issues. Once reformatted:

UIView.animate(
  withDuration: 0.3, 
  animations: { self.view.alpha = 0 }
) { _ in self.view.removeFromSuperview() }

I can see the problem quite clearly; it's about the role of the final closure. Looking back at the proposal's example of successful trailing closure use, we find:

UIView.animate(withDuration: 0.3) {
  self.view.alpha = 0
}

What makes it OK that the trailing closure is unlabeled in this case? For me, it's fine because it represents the change that's being animated, in other words, it has a primary role in the call that can be deduced from context—and I see that the proposal identifies “primariness” as the reason trailing closure syntax usually works out. The issue with the “problem” example is that we've put a closure with a secondary (and inconsistent!) role in the primary position at the end of the call. If the example with one closure is good, I suggest this should also be considered good:

UIView.animate(
  withDuration: 0.3, 
  completion: { _ in self.view.removeFromSuperview() }
) {
  self.view.alpha = 0
}

And if you're going to argue that the role of the trailing closure isn't clear enough in this example, I'd ask that you explain why it's clear enough in the example with a single trailing closure, and not here.

To sum up, the one example cited as a problem suffers from poor Swift API design, because it wasn't designed for Swift—it was imported from Objective-C. Before we complicate the language with new wrinkles on calling syntax we should have—not just one, not just a few, but many— examples where there are problems that couldn't have been solved with better API design choices.

Problems With the Solution

I have nothing in principle against extending the expressiveness of the language by making it possible to label trailing closures, nor with features that reduce nesting. This proposal, however, moves the de-facto position of the primary closure from the end to the beginning of the parameter list. Existing multiple-closure APIs that were sensibly designed to take advantage of the status quo now have an even terser syntax that puts a label on the trailing closure. So, for example, if the closure order in UIView.animate had been well-chosen, users could write their call as in my last example, or they could write:

UIView.animate(withDuration: 0.3) { _ in
  self.view.removeFromSuperview() 
} animations: { self.view.alpha = 0 }

It seems to me that the above is no better than the original when it comes to clarity, at least if you buy the idea that the problem is that the secondary closure needs a label.

By adding more options for how to call a given function, we are taking control away from API authors over how their APIs look in use, and transferring it toward users. That's not necessarily wrong; API authors can't control how users will format their code or name their variables, both of which can have a huge impact on how well an API's use site reads; clients have to participate in making use sites read well.

If we're going to take this tack, though, let's start with a simple change that addresses the only real problem raised here: sometimes you need a label for the trailing closure in order to make its role clear, and to use it you have to give up on the de-nesting benefits of trailing closure syntax. Just allow the existing trailing closure syntax to be optionally labelled for clarity.

Problem of Focus

This proposal seems to throw a relatively large change at a very small, syntactic problem, as far as I can tell. IMO there are enough real fundamental problems to solve in Swift (ownership, CoW slice, variadic generics, concurrency, … off the top of my head) that if we're going to “do syntax sugar,” we should keep the changes small and conservative, and we should be rigorous with ourselves about the problems actually being solved, so we can put attention on the fundamentals. This proposal seems to do neither.

43 Likes

I don't think you've gone quite far enough in explaining why the chosen example is poor. It's not a bad API design. It's an API inherited from Objective-C, and it fits in very well with Objective-C syntax. When written out, you can read the closures as naturally following each other in time, as we would expect with an animation which is followed by a completion (handler).

Even in Swift, if one ignores the trailing-closure option, one could write:

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

(The fact that Xcode won't format it nicely in most cases is an Xcode problem, not an API or syntax problem.)

I don't find the fact that one can find examples which don't work nicely with trailing-closure syntax a compelling argument in-and-of-itself. The answer isn't to add yet-another-option which will inevitably have its own list of examples which work poorly. The answer is to use the syntax that is already available.

14 Likes

I agree with your concerns expressed in the "Problems with the Solution" section. I think that optionally labelling the first closure should be more thoroughly considered and as some other members of this community suggested, I advocate the deprecation and eventual removal of implicitly dropping the label of the first closure (in future versions of the language, of course). I believe that, because not all APIs have a first closure whose label is implied by the function/method name. There has been recent discussion about Standard Library methods have closures whose purpose is unclear due to the lack of a label (example: Renaming Trailing-closure Functions in the Standard Library).

I, also, understand your concerns regarding the focus of the community on what I assume you'd call unimportant language features, but I firmly disagree.

What I don't support is the fact that in the "Problems With The Problem" section you review the writing of the proposal, not the ideas shared in that proposal. The purpose of this discussion is to provide constructive criticism and express our opinions about the proposed change, not judge members's writing abilities. Providing constructive feedback on the writing is, of course, welcome. Even for that though, I consider this thread unrelated.

3 Likes

You're right; it's a poor API design for Swift as it is currently defined. I'll amend my post to that effect, thanks.

Lest we forget that things like this TextField(_:value:formatter:onEditingChanged:onCommit:) is tricky to design with current syntax.

3 Likes

Please support your claim with specifics. My objections in that section are, as far as I can tell, absolutely with the ideas expressed in the proposal's problem description: they fail to convince me that the proposal's authors understand what problem they're trying to solve, or that any problem they are solving is real. While the ability to make a convincing argument is always going to be tied, at some level, to writing skill, I wouldn't be criticizing coherent, compelling ideas expressed, for example, with poor grammar.

2 Likes

How should Xcode (or any formatter) know when it's preferable to use the trailing closure syntax and where it should be avoided in order to preserve the label? At best it'll know about a limited list of functions hard coded into the tool, or it'll use a heuristic that'll guess wrong half the time.

Whereas if there was a way for function declarations to tell when the label is important, code formatters would have a useful signal to work with. Arguably we already have that with _ labels, but the trailing closure syntax treats both _ and non-_ labels equally so it's not very useful in that regard.

2 Likes

Simple – if the function takes more than one trailing closure, don't use the trailing closure syntax.

It might make sense to invent a more advanced logic, but that's an issue with Xcode anyway, not with Swift

3 Likes
  • What is your evaluation of the proposal?

+0.25

If the first closure also had a label, +1

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

Yes. Trailing closures have become an important and popular language feature and making them work for multiple closures will make more APIs feel Swifty.

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

For the most part yes. But to me the special case of having one unlabeled trailing closure feels like a big mistake. It is reminiscent of the way argument labels worked prior to Swift 3, where the first argument to a function automatically had its label suppressed. While it was initially appealing for ObjC interop, it ended up not feeling right for Swift and in Swift 3 the special case for the first argument was removed and now we use argument labels everywhere (unless the author of a function decides that a particular argument works without one).

I think the same thing is bound to happen here (just look at the other feedback in this thread!) so we should probably learn from the past and avoid that mistake entirely. All trailing closures should be labeled from the start. For a function with a single trailing closure we should allow the label to be elided for compatibility with current source, but we should consider dropping that in the future. Perhaps we should allow an individual function to specify that the label may be elided (either by setting it to _ or with some @attribute) but that can be decided later after we have more experience using this syntax.

  • 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 been following the threads pretty closely and have played around with some use cases.

4 Likes

I’ve been staying out of this review and waiting for things to settle, but that doesn’t appear to be happening so I’ll offer my perspective now.

With regard to existing APIs, I find the proposal’s motivation quite lacking. The examples it gives can be written perfectly clearly with the existing standard syntax, not using trailing closures at all.

Trailing closures are a tool, and it is up to the programmer to decide when that tool is helpful for the job at hand. Sometimes it is better to write out the function call long-hand, with all labels included and all arguments inside the parentheses. That is a perfectly valid thing to do.

A side-by-side comparison of examples from the proposal using standard (non-trailing) syntax, and using the proposed syntax, shows almost no change. The proposed syntax just moves the closing parenthesis and drops a few commas. It is not obviously a win at all, especially not a large one.

• • •

Others in this thread, including @Lantua and @dabrahams, have mentioned that many existing APIs have a “primary” closure. For those functions, it would often make sense to use trailing closure syntax for the primary closure.

However, some of them place the primary closure in a position where it currently cannot be used as a trailing closure. That is a specific and identifiable problem, which could be solved with a @trailable attribute.

• • •

The proposal has not provided compelling examples of new APIs that would benefit from the proposed syntax. If the authors have ideas in mind where they would like to use this proposed feature, that should be prominently placed in the motivation section since it would in fact be their motivation.

The absence of any such examples for new APIs with this feature, is a substantial mark against the proposal. If it does not enable new code to be written in a demonstrably-superior way, then it should be flatly rejected.

Instead, the motivation section spends all its time discussing legacy Obective-C APIs that were not originally designed for Swift at all. In my view it would be far more productive to consider ways in which those APIs could be improved or replaced to become more Swifty.

Moreover, as mentioned above, any such hypothetical new API that purportedly benefits from this proposal, could just as easily be called in the standard way, with all arguments inside the parentheses. With such a straightforward alternative already existing, the bar is quite high for introducing additional complexity, and the proposal at hand effectively just allows a few pieces of punctuation to be shuffled around.

18 Likes

There are three examples in the proposal. UIView.animate, whose signature would not change, and Combine.sink and SwiftUI.Section, whose signatures would change.

1 Like