SE-0279: Multiple Trailing Closures

yes

The switch analogy kind of works, but it falls apart when you consider that the code paths will likely have different return types. It also seems like having a closure that isn't surrounded by braces could be less clear / more confusing.

Could this be handled with an internal type which is returned as some YourNarrowProtocol? The only thing you'd be able to do is apply the .success member.

It would be really nice if we had support for full partial application some day, rather than emulating it with wrapper types. You can do it with closures, too, but it’s kind of awkward to write and to use.

Yeah, that could work, and would be a nice way of doing things if you do want to be able to incrementally attach callbacks to the result of tryToDoIt(foo). OTOH, it'd still be unfortunate to have to define a separate protocol in order achieve this syntax if success and failure are effectively requirements of the API.

You'd also have to define a family of protocols as well as conforming types to handle the sequence of "trailing closure" parameters. Even with anonymous structs as the conforming types, this would still be a ton of boilerplate and a significant reduction in clarity of the API for both the user and the implementer.

I really don't think this approach is a viable alternative to finding a suitable syntax for multiple trailing closures. Chained operator syntax works is most appropriate when you have a lot of operators that all work on the same type constructor (as in ReactiveSwift) or family of types that all conform to a known protocol (as in SwiftUI and Combine).

Consider the alternative on balance: changing the language to add sugar. If the syntax optimization desired here is critical for a few specific APIs, then adding a few types and helpers might be worth it, vs changing the language for everyone.

4 Likes

Sure, if it was just a few APIs then that might be true. But I come across APIs that accept multiple trailing closures relatively often. This is not an edge case. It shouldn't require sophisticated library design techniques which expose an unusual looking API surface to users.

I've always considered this to be a small but nontrivial area of friction in Swift. This proposal may or may not be the optimal solution - we've seen a lot of new territory explored already in this thread - but I believe the problem is well motivated and deserves a solution.

3 Likes

Yes it can. I have used this with a new SQLite wrapper library I’m making based on opaque types and function builders.

  • What is your evaluation of the proposal?

+10 Love it. Why isn't this already in the language?

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

Considering the clarity I feel that it provides and the fact that it doesn't introduce any breaking changes to existing code I think this is a win/win solution.

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

We're about to see multiple trailing closure patterns used a lot in SwiftUI (the button example in the proposal) as well as in Combine sinks.

// old
publisher
    .sink(
        receiveCompletion: { e in
            // handle e
        },
        receiveValue: { v in
            // handle v
        }
    )

// new
publisher
    .sink {
        receiveCompletion: { e in
            // handle e
        }
        receiveValue: { v in
            // handle v
        }
    }

As such I echo my original comment: Why isn't this already in the language?

The new format also matches fairly well with computed property labels, though I'm a bit on the fence as to whether or not we want the : following the attribute name, or if we should simply match the computed property label format.

var s: String {
    didSet {

    }
}

I can argue it either way, so I'd be good with either approach. But that said, requiring the : does reduce the syntactic complexity.

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

Read the proposal and waded through ALL of the commentary.

1 Like

After reading most of this thread and the proposal I'm a slight -1 on the current version. I understand the utility, but:

  1. It's yet another choice for closures, when there are already so many (e.g. { (arg) in ... } vs { arg in ... } vs { $0... }, trailing vs. not, etc...)
  2. As far as I could glean, it only addresses closures in the final positions. That's the most common case, but not the only painful one. func foo(bar: () -> Void, baz: Int, bang: () -> Void) may go against convention, but it's not disallowed.

I'd be happy just to fix the tooling to avoid suggesting trailing syntax when there are multiple closures. However, if a change were made I think it should be general like DevAndArtist's suggestion.

If the solution were more general, even functions with no closure arguments can be improved. SwiftUI's .overlay() is currently pretty ugly to use IMHO:

		Color.red.overlay(
			VStack {
				Text("Hi")
			},
			alignment: .trailing
		)

This could become:

		Color.red.overlay {
			VStack {
				Text("Hi")
			}
			alignment: .trailing
		}

Which to me feels a bit more consistent.

2 Likes

I like this idea.
I oppose original proposal, but support this.

I think the most important advantage of trailing closure syntax is avoiding double paren like }).
This idea have this too.

But I worry about syntactic ambiguity between two trailing closures and one trailing closure which contains jumping label.

For example, following code is valid in current Swift.

func f(_ f: () -> Void) {}

f {
start:
  while true {
    break start
  }
end:
  while true {
    break end
  }
}
2 Likes

Right, I get that. The question isn't whether APIs with multiple trailing closures exist - that is clearly true. The question is: "to what lengths are we willing to go to further sugar what we already have?"

Objectively, I think you will agree: 1) We already support APIs with multiple trailing closures. 2) They aren't as syntactically elegant as APIs that take a single trailing closure. 3) We care about the language being consistent and explainable, and the bar for sugaring syntax things is fairly high.

Sure, as I mentioned upthread, I agree this is a problem worth solving if we can come to a good solution.

All I'm saying is that, in my opinion, this specific proposal is not the right thing for Swift on balance: taking it would make the language worse overall. There are other approaches that should be investigated, so I think they should be investigated in general discussion or pitch sort of thread, it is not correct protocol to debate alternative solutions in this proposal review thread.

If this specific proposal is the wrong one, we should reject it and continue discussing other solutions elsewhere.

22 Likes

-1 on the proposal. It seems the intention is to make the visual appeal of code more and more pleasing, whereas actually comprehending what is written gets harder and harder, especially for newcomers.

For me, in this regard, the proposal falls into the same category as function builder syntax:
Anyone new to the language will never realize what it actually is (I mean no one would even guess that the outer context takes the unused return types of what are just functions…).

So it becomes just "ok, this is how it is done" :man_shrugging: , and I perceive this to be a serious flaw in language design. The easier it is to understand what it is you are looking at in a code snippet the better. I think single trailing closure syntax strikes a good balance, but beyond that it is hard to justify for me.

The similarity in syntax with loop labels is another nail in this "just visually pleasing" coffin.

13 Likes

What is your evaluation of the proposal?

+1

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

As soon as an API has more than one closure, the code loses its readability. Until now it only affected a few APIs but with frameworks like SwiftUI or Combine it is much more often that the case occurs. That Swift is evolving in order to facilitate the use of this type of framework seems quite logical to me.

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

Closures under Swift are very powerful and often used. So it seems to me a good thing that Swift makes it easier to use.

1 Like

You have an error in the “new” version of your code. You added an extra } at the end. I'm not pointing this out to be pedantic! I'm pointing it out because I believe SE-0279 syntax encourages this sort of error by adding another meaning to braces.

8 Likes

It also eliminates the unsightly }) that can become a major nuisance

In my opinion, in the example given the ) is a helpful delimiter. It makes the end point of that nested call easy to pick up when scanning. And in general, having a closing paren for the end of a function call makes it stand out nicely among the many curly braces in Swift.

Perhaps it's true that this might not hold in SwiftUI, but the vast majority of Swift code is not SwiftUI.

I don't think this proposal would be a good addition to the language. It's helpful for reading code when different things look different. This uses the same curly braces used elsewhere for something quite different*. And although I'm not quite "on team gofmt", so to speak, I do think this exceeds the limits of judiciousness in spelling variations. This change doesn't pull its weight; it adds a new thing to learn to read (and/or to write style rules about) without aiding comprehension.

I followed the pitch thread, read the proposal through a few times, and have read most of this thread. (Side note, I'm also rather surprised that this went from pitch to proposal so quickly, and, as @xwu noted above, without really taking any of the pitch feedback into account.)


*Closest comparison is a switch, but there the flow of execution is not hidden away in an arbitrary other place.

1 Like

This gets to the heart of my unease with this proposal. I remember sometime early on in Swift evolution, @Chris_Lattner3 pointed out that ”it’s better for clearly different things to be... different”. The proposal breaks that principle. It makes parameter lists (usually delimited by ()) more similar to statement blocks (usually delimited by {}), even though they are clearly different things. That adds conceptual complexity to the language, making it harder to learn.

Imagine encountering this in code for the first time, especially in a function builder, where many of the normal rules are already out the window. I think there’s a big risk that you wouldn’t understand the underlying principle. You might think ”Weird. Well I guess that’s just how you write Button. :man_shrugging:”, but in the worst case you gain an incorrect understanding: ”Oh, this must be one of those closure things, except it has has two cases, just like a switch.”

18 Likes

This was partially why I suggested the 'fluent' approach to this problem above - IMHO it's a little bit more clear how the closures relate to the function/entity they are decorating or changing, and most people are familiar with fluent interfaces anyway.

Of course as people have mentioned fluent APIs have their own issues and behind the scenes boilerplate, I was just wondering whether there were some language enhancements that could address the drawbacks, and if that might be better than the proposed multiple trailing closure syntax.

There's another aspect that makes parens helpful: it's much easier for the compiler to produce correct diagnostics in the presence of mis-nested delimiters. A missing } or an extra { will change the scope of all the code below in the file, generating plenty of surprising errors until the end of the file. Whereas a missing ) or extra ( has a much more localized effect.

6 Likes