SE-0279: Multiple Trailing Closures [Amended]

  • 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

What DSLs? Can you point to an accepted proposal?

SwiftUI uses non-standard extensions that nobody else can use; it isn’t Swift. That means we basically have to take the word of Apple‘s framework team that this is the best design to solve the problem - which is something I’m frankly not willing to do, and I don’t think it’s enough to make such a massive change to the language.

I’ve seen Apple engineers apply Swift with varying degrees of competence (which is not to slate those engineers or Apple - it takes a lot of time and experience to learn how best to design APIs in a new language, and ideas evolve over time). Different teams also appear to have different ideas about best-practices (e.g. Foundation‘s Data is it’s own slice type, something that the standard library explicitly rejected for types like Array and String).

Maybe there is a much better design for sections which doesn’t involve trailing closures. I don’t know - I can’t use the same features that SwiftUI does, so I can’t explore it. If I could, maybe I’d come back and say that I agree with the SwiftUI team that this is absolutely necessary and worth the complexity, but as things stand I‘m supposed to just take it on faith.

6 Likes

I sympathize with your concerns about proposals passing without community review and feedback. You’re right SwiftUI uses a non-standard feature, but that definitely doesn’t mean it’s not Swift. All this has been thoroughly discussed in another thread and I believe that is not the right one to continue this discussion.

Despite your concerns about Apple “overly controlling” the language (feel free to correct me if I misunderstood), I think that this is arguably a good addition to the language. For those unwilling to use it, I think that Editor Formatting Tools would be a better course of action than the prohibition of multiple trailing closures.

Besides, it’s likely that once Function Builders become an official language feature, much more libraries will use DSLs and would benefit from the proposed feature.

4 Likes

I think Multiple Trailing Closures has its merits in writing different categories of params especially for long block of function/closure arguments.

For example, func F(v,f,v,f,f,...) take mixed var/func params, with MTC we put var args in (v...) and trail func args to the end of (v...) f: {}..., as developers we simply put value params in (...) first, and write long closure argument slowly over time at the end of function one by one.

fun (v...)
c1: { take M mins...}
c2: { take N mins...}
...
_ : {maybe take hours...}

different part of code take different type/category of params var/func.

The problem is how to write it conveniently and clearly, both for dev to read and compiler to parse.

Label everything, the first one can be omitted/optional for traditional single trailing closure compatibility probably is the common answer to this pitch.

1 Like

There was discussion about adding Function Builder functionality to the Package manifest. With MTC I think it would look really great:

let package = Package(name: "MyPackage") {
    Executable("App", targets: "AppTarget")
} targets: {
    Target("AppTarget")
}

EDIT: It was said that the above code doesn’t clearly show that the unlabeled closure is meant to represent the package’s products and I agree. I thought that was obvious and that I didn’t have to explicitly point this out (my bad). Such an implementation would not be really expressive with the currently proposed feature. Because of that I also don’t think it should be added without the ability to use labels in all the closures. Nonetheless, I do feel it is a step in the right direction as it holds great potential.

It is a concern, but not the point I was making, so you did misunderstand.

I’m saying that there are many alternative designs for sections in DSLs which won’t use multiple trailing closures. This community, which routinely demonstrates novel API patterns, has not explored those alternative designs because we can’t use function builder DSLs in our own libraries.

We are instead left to assume that SwiftUI‘s solution is the best there is, and that multiple trailing closures must be necessary. That is not a very compelling argument IMO.

Perhaps, if it is a common need in that particular context, it could be limited to function builders (which are very unlike regular Swift closures anyway). Again, lots of unexplored options because function builders are not currently part of Swift.

6 Likes

I realize it's already past the review deadline, but I just want to add a quick comment on the suggestion about SwiftPM package declaration syntax by @filip-sakel.

There seems to be no indication on what the first closure is about, and it makes the declaration difficult to understand even for developers who have worked with SwiftPM previously, and pretty much impossible to figure out for newcomers.

This is actually a great example of how well-designed APIs might become a lot less comprehensible if this proposal is accepted.

15 Likes

I'm imagining SwiftUI code with a couple extra () and it seems fine. The commas aren't unnecessary, the labels aren't unnecessary. And nobody is bothered to have typed them.

But like I said, we could just type function callers like this myFunction 100, {} {}, true and have no labels and have no (). In fact, we could look for every case in swift code where the parser/compiler could guess what you're trying to do and just do that so we don't have to type anything. Do we even need the func keyword? or var keyword? Many language don't have them. Imaging not having to type func or var at all, It would really clean things up.

UNRELATED OPINION
I think that Function Builders are a hack job there require too much explanation for how they work just to explain why you can't do a proper IF statement of Switch (among a million other basic things that would be solved if we didn't have function builders at all). It's a dissertation that isn't worth the trouble. It also doesn't solve a problem except typing .addSubview() and allowing the chaining modifying functions. Which is NOT clean. I'm disappointed that it wasn't well thought out before being adopted. I prefer to stay off this topic but my bias is necessary to understand my perspective here.

2 Likes

Agreed.

Function builders are only partially implemented. This will work when the feature is completed.

5 Likes

I love this proposal and think it is a net positive for Swift in it's current form. I do have a few small concerns.

As @bjhomer pointed out above, we used to special case the first parameter label for functions and we removed that. It simplified the rules for how parameter labels worked, and made learning the language easier.

Prior to SE-0046, there was a way to opt the function in to having a label on the first parameter, an affordance we don't have with this proposal for labels on the first trailing closure.

If the author of an API wants to elide the first parameter label, they can opt in with an _. We could go the other direction and default to no parameter label, and have a way to opt out (I believe you did this by adding a # or some other character for the first parameter label in a function), but this adds extra syntax and I don' think it's worth it.

We can (in a future proposal perhaps) consider deprecating the behavior in single trailing closures of dropping the label by default to bring all trailing closures in line with each other.

Summary: +1 overall, but lets get rid of the special case for the first trailing closure.

7 Likes

I agree with @dabrahams.

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.

The real issue is that API designers don't have control over usage of trailing closure. For APIs with more than one closure and which all the labels are required for clarity, the API designer should be able to disallow trailing closures, which would fix the problem much more elegantly.

1 Like

Hi everyone – the review period has now ended and the core team will post an update on next steps shortly.

12 Likes

Proposal Accepted

The core team has accepted the amended version of this proposal.

3 Likes
Terms of Service

Privacy Policy

Cookie Policy