[Accepted] SE-0279: Multiple Trailing Closures

While I’m disappointed in the outcome of this review (I’m for the breaking change to trailing closures in general), I’m not really disappointed in the process per-se.

I do believe though that my interpretation of it is probably not the same as most of the community’s, and that would need some work. I think that it needs clarifying (apart than in this thread which has limited visibility) that the language evolution is ultimately a core team issue, and the community’s role is to help in identifying issues only

Edit: I realise I’m implying that my interpretation is the correct one. Of course that doesn’t have to be the case, but an official clarification would help in this case as well :D

1 Like

SE-163 and SE-180 were reviewed prior to Swift 4 – almost half Swift’s current lifetime ago, when Swift was less established. Such changes to a fundamental type would be similarly limited now as a change to single closure trailing syntax would be.

Remaining neutral on all the criticism about the proposal and the process, this seems like a compelling question. A satisfying answer might help quell the backlash.

Objections to the proposal as accepted all center around situations where two closures have a symmetry in purpose that demands a symmetry in labeling. Finding a way make that situation read well under this accepted proposal is important for the health of the language ecosystem, even if (especially if) this proposal is not up for further debate.

Perhaps there is a naming convention that resolves this. Perhaps in situations like that it’s best to keep the closures inside the parens. Perhaps there is even some coherent readability rationale behind that. I don’t know! But it could be helpful for all concerned to see that line of reasoning worked out thoughtfully. Perhaps community members who like the proposal as accepted (I don’t want to put this all on the core team) can think through a few examples, in the spirit of the existing API Design Guidelines.

13 Likes

Assuming swift continues to be successful (Which we should assume else what’s the point) then in the future it will be even more established and people will look back on this time and wonder why the truly desired changes weren’t made back when swift was less established.

12 Likes

I understand your viewpoint of Swift stable api/abi law. But source breakage restriction shouldn't be the hurdle of getting evolution/better changes to the language.

Swift only at age of 6 this year, the more we follow the law of stable/nonbreaking the less we could make changes to the language itself as time goes. At the end, Swift will not be Swifty any more.

From python 2 to 3, from C to Obj-C to Swift, nothing is un-breakable in the history. Keeping modern, clean, concise and elegant is Swift's core value, if changes can bring us more merits why keep old-stuff unchanged; breaking change is everywhere in language evolution world.

20 Likes

I want to quote one core team opinion which I had to quote last year as one of the proposals I pitched during Swift 3 evolution was turned down, but because SwiftUI came around WWDC19 was resurrected and waved through the language evolution. I don't know if Chris still has the same opinion or if it's the same about the current proposal or not, but here it is, a core team opinion that "syntactic sugar proposals are dangerous and should only be considered at last of the language evolution".

1 Like

I have always been able to understand and accept the core team's rationale even when I disagree with it. In this case though, I frankly cannot fathom why the core team thinks a multiple trailing closure feature that does not work with a whole class of APIs (those where all closures are peers and require labels) is acceptable. Binding.init and Result.fold are common examples that have been discussed, but they are not alone. I have several such API in the library I'm currently working on. This specific concern was prominently discussed in the review thread, is substantial and compelling, and is not mentioned in the rationale at all.

The proposal itself does mention an alternative that would address this concern by optionally allowing the first closure to be labeled. Yet that alternative does not mention an extremely compelling use case. It waves it away by making reference to the new API Design Guideline which was included in the proposal, a guideline which also entirely ignores this class of API and would have us consider them poorly designed API. In fact, this family of API demonstrates that the guideline is flawed: these API are made worse and less clear at the point of use when the leading label is dropped, thus violating the very first fundamental principle of the API design guidelines themselves: Clarity at the point of use.

The alternative mentioned in the proposal is perfeclty compatible with the core team's desire to make multiple trailing closures an extension of the existing feature, yet the rationale did not discuss whether it was considered by the core team or why it was rejected.

The community is speaking up loudly because accepting this proposal as-is is a mistake that we will regret (unless it is at least amended or followed up with support for an optional label on the first trailing closure).

There were also plenty of us who were initially supportive, but later came to realize how important it is to add the first label. I never explicitly chimed in to revise my review to "for, but only if you can add the first label", but that is the viewpoint I came to share. My assumption is that it was evident in the later comments I left in the review thread. It now seems to me that my assumption was flawed. I would not be surprised if others are in the same position.

Exactly. Sometimes omitting the first label works because the base name makes the purpose of the first closure clear. In other cases it is impossible to design an API that does so without feeling out of place in Swift (by baking what should be an argument label into the base name).

35 Likes

Yours was one of the five votes in the "for (as proposed)" column. Now that you've said that, that would make 4 in support, and 19 in support if including the first label was an option.

1 Like

Honest question, @John_McCall: Would a proposal for amending this decision to allow for the first closure to be (optionally) labeled even be considered or would that be rejected right away?

34 Likes

I am also wondering what specifically is the core team unsatisfied with about trailing closures? I am especially interested wrt this proposal.

By knowing where the core team thinks things can be improved is a great place for us as a community to look and find some solutions.

2 Likes

It's actually possible to get the desired call-site API here for something like fold or Binding if you're willing to toss in a throwaway parameter:

struct Binding<T> {
    init(gobble: (()->Never)? = nil, 
            get: @escaping ()->T, 
            set: @escaping (T)->Void) 
    {
        self.getter = get
        self.setter = set
    }
}

Binding
  get: { return 42 }
  set: { print("setting \($0)") }

It's kinda an icky workaround, but people will probably do it because it allows the call site that people are trying to craft at the point of use.

Given that this can be done, people probably will do it. Is that an acceptable outcome?

13 Likes

I don't see how that works with the current design.

I'm not sure this is accurate. Per the proposal, the trailing closure grammar is amended to:

expr-trailing-closure:
  expr-postfix(trailing-closure) trailing-closures

trailing-closures:
  expr-closure
  trailing-closures (identifier|keyword|'_') ':' expr-closure

AFAICT, the grammar disallows a label before the first trailing closure, so the example you've posted would fail to parse.

2 Likes

Ah, you're right. There's an example specifically called out in the proposal with a defaulted parameter like this.

I disagree. I will go so far to say that exactly because of the no-first-label thing, there are no functions that will work well with the "asymmetric form".

Say if someone designs a function like this:

func foo(bar1: () -> (), bar2: () -> (), bar3: () -> ()) { /*...*/ }

and expected people to use it like this:

foo { /*...*/ } bar2: { /*...*/ } bar3: { /*...*/ }

But because of the flexibility the user has over where the trailing closure starts, it can be used like this:

foo (bar1: { /*...*/ } ) { /*...*/ } bar3: { /*...*/ }

or like this, the most backward-compatible style:

foo (bar1: { /*...*/ }, bar2: { /*...*/ } ) { /*...*/ }

Not a single function can be designed to handle this kind of flexibility and inconsistency. Labels intended to be omitted are not, and those designed to stay are omitted.

11 Likes

You'd still have this:

Binding {}
  get: { ... }
  set: { ... }
4 Likes

I think we’d like to get some actual living experience with this design before considering a revision, but yes, it was intentional that we picked a design that can extended in that way in the future.

5 Likes

If the decision on this proposal stays, and if a new proposal for optionally adding the first label is disallowed, then this is a regression to the pre-SE-0046 days.

3 Likes

I already specifically callout this during the review. And the core team does seem to acknowledge it (from the announcement).

1 Like

Why do we need to spend time living with a design which has an obvious and glaring flaw before extending it with a viable solution? If the answer is WWDC I think everyone would feel better if there was some honesty and openness around that.

21 Likes