SE-0279: Multiple Trailing Closures

This has been a minor rough edge for me since 1.0. Like others have mentioned, it's not heinous, just somewhat unsightly. I am in favor of doing something to address the imbalance here.

I have concerns about the label: { } syntax. It's feels like it's related to named labels for control flow statements, but isn't related at all.

I slightly prefer the version without : mentioned because it looks like willSet et. al. for properties, and composes nicely with that feature space to me. I understand the ambiguity argument, though.

+1 for solving this problem and +1 to the proposal as written, with the -0.5 caveat that I prefer another spelling.

1 Like

What is your evaluation of the proposal?

-1. The proposed feature does not pull its weight.

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

No. The proposal pits the proposed syntax against the existing syntax of writing closure in-line. However, it fails to acknowledge an alternative: naming functions to be passed as arguments separately.

For instance, rather than writing the following:

Button(action: {
  ...
  ...
}, label: {
  Text("Hello!")
})

One might write:

Button(action: buttonTapped, label: {
  Text("Hello!")
})

// elsewhere:
func buttonTapped() {
  ...
}

Breaking out the bodies of closure arguments separately provides an opportunity to name them explicitly and separates the action logic from the structure of the view hierarchy (in this SwiftUI example).

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

Swift strives to be an opinionated language, but this proposal provides an additional syntactic spelling for a feature that already has multiple spellings. Arguably, this proposal unifies that front with a recommended spelling; in practice, I worry about fragmentation.

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?

Followed the original pitch thread and read the finalized proposal.

3 Likes

-1, for many of the same reasons already mentioned above. I'll quote @xwu since I think this gets to the crux of the issue:

Calling this "multiple trailing closures" is false advertising, IMO. As he points out, this isn't an extension of trailing closures—a trailing closure is a closure that lives outside of the other parameters of the function call and has its label elided. I'll grant that you can't avoid using the labels when there are multiple occurrences, but by adding the extra curly braces around the closures, all this syntax does is swap out some punctuation but not much else. Therefore, this change would make the language less clear, not more clear, because there would now be two forms of these calls that look almost identical except for punctuation:

foo(bar, baz) {
  quux: {...}
  florp: {...}
}

foo(bar, baz,
  quux: {...},
  florp: {...}
)

Indeed, the motivating examples in the proposal are artificially inflated because of the way they're formatted. The readability problem, if there is one, can be solved entirely with formatting, by ensuring that (1) no function call contains both a trailing closure and a closure in its labeled argument list, and (2) each closure is on its own line, as I have above. This does not require a new nearly-identical syntax be added to the language.

At least with single trailing closures, the goal was to allow for the creation of DSLs that provide a block-like syntax similar to existing conditional/loop constructs. This doesn't do that either, because of the requirement that the closures be nested inside their own {...} and that they still have their argument labels. A proposal for multiple trailing closures must, in my opinion, follow the same philosophy in order to be consistent. The proposal does acknowledge this in the Alternatives Considered section and rightfully calls out that doing so would be difficult to parse without introducing syntactically significant whitespace that would conflict with some users' formatting preferences. But that does not mean that we should accept this alternative syntax instead as a substitute.

Given the history of style/formatting discussions across the history of computing, this is a very bold assertion. :slightly_smiling_face:

16 Likes

I'm -1 on this in its current form. The examples of how the current syntax hurt readability are excellent and show that improvement is needed, but the examples of how it would read given this proposal only seem to get us halfway there, and this feels like a halfway step that might paint future syntax of something better or even unrelated into a corner.

  • What is your evaluation of the proposal?

-1. In my opinion this problem can be solved by better code formatting.
This proposal just replaces () with {} and removes the , between closures.
Below I have listed all samples from the proposal, and in my mind the current syntax and the proposed syntax are equally readable and usable.

UIView.anmiate sample:

before:

UIView.animate(withDuration: 0.7, delay: 1.0, options: .curveEaseOut,
  animations: {
    self.view.layoutIfNeeded()
  },
  completion: { finished in
    print("Basket doors opened!")
  }
)

after:

UIView.animate(withDuration: 0.7, delay: 1.0, options: .curveEaseOut) {
  animations: {
    self.view.layoutIfNeeded()
  }
  completion: { finished in
    print("Basket doors opened!")
  }
}

toggle sample:

before:

func toggle() {
  UIView.animate(withDuration: 1,
    animations: {
      self.myView.backgroundColor = UIColor.green
      self.myView.frame.size.width += 50
      self.myView.frame.size.height += 20
      self.myView.center.x += 20
    },
    completion: { _ in
      UIView.animate(withDuration: 1, delay: 0.25, options: [.autoreverse, .repeat],
        animations: {
          self.myView.frame.origin.y -= 20
        }
      )
    }
  )
}

after:

func toggle() {
  UIView.animate(withDuration: 1) {
    animations: {
      self.myView.backgroundColor = UIColor.green
      self.myView.frame.size.width += 50
      self.myView.frame.size.height += 20
      self.myView.center.x += 20
    }
    completion: { _ in
      UIView.animate(withDuration: 1, delay: 0.25, options: [.autoreverse, .repeat]) {
        animations: {
          self.myView.frame.origin.y -= 20
        }
      }
    }
  }
}

Button sample:

before:

Button(
  action: {
    ...
    ...
  },
  label: {
    Text("Hello!")
  }
)

after:

Button {
  action: {
    ...
    ...
  }
  label: {
    Text("Hello!")
  }
}

when sample:

before:

when(2 < 3,
  then: {
    ...
    ...
  },
  else: {
    ...
    ...
  }
)

after:

when(2 < 3) {
  then: {
    ...
    ...
  }
  else: {
    ...
    ...
  }
}
  • Is the problem being addressed significant enough to warrant a change to Swift?

No.

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

No

  • 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?

Read the pitch and the proposal.

5 Likes

Moreover, I should point out that SE-0257 (elide commas in multiline expressions) was returned for revision, not rejected. Therefore, there are two related proposals related to commas in-flight. If SE-0257 were to be adopted, the result would be as follows:

// SE-0257
foo(bar
  baz: { ... }
  boo: { ... }
)

// SE-0279
foo(bar) {
  baz: { ... }
  boo: { ... }
}

Personally, I disagree that commas are detrimental to clarity; in fact, I find them to be quite helpful. But if it were Swift’s opinion that these commas must go, I would prefer a more holistic treatment as in SE-0257.

10 Likes

I'm a solid -1 on this proposal. I share the same concerns outlined by @xwu and @allevato about introducing an additional spelling for the very marginal gain of omitting a , and using ( and ) instead { and }. With decent code completion, I've never had an issue writing UIView animation calls, and when reading the difference between { and ( is so negligible to me as to be unnoticeable. The wins here are not clear enough to instill confidence that this new syntax would take over and leave the "old way" as a vestigial part of Swift syntax. Instead, I forsee a future where opinions remain divided and the usage of both syntaxes is pervasive. That world seems clearly worse to me than one that uses the current syntax exclusively.

2 Likes

I read an interesting comment on Slack from @Paul_Hudson that some of the motivations behind such proposals could again be private to Apple as we're not far away from WWDC. Just remember the situation we had last year. Obviously without knowing that, assuming there are any private motivations, it's a little bit hard to tell why we would need an alternative spelling. I'm personally neutral on this proposal. I'd probably use the syntax if it was available.

Also remember that many of us wanted to get rid of return in single expression context, but got pushed back by the core team, and yet because of things like SwiftUI we got that feature anyways.

2 Likes

All well and good, and the Core Team is welcome to base their decisions about the proposal based on that. The only thing that makes sense for the community to do here, though, is to consider the proposal based on publicly known information. Speculating on Apple's future uses of a proposed feature or tempering criticism because "maybe Apple has something up their sleeve" seems... misguided. Considering those as-yet-unknown uses and weighing them along with the community's feedback is the job of the Core Team.

4 Likes

Sure, but it has to be said out loud by someone. It doesn't hurt anyone. :slight_smile:

May I quote what @Paul_Hudson wrote on slack:

Before WWDC19 there were some features that were very much “that’s neat, but why?” and now it’s obvious. It’s hard to tell whether new features such as multiple trailing closures have impact on something we haven’t seen yet.

-1, for the same reasons others have already mentioned.

+1

I am very much in favor of this proposal. The difference between accepting one closure and accepting "even one more" closure has been a point of annoyance for me for a while. While the 'amount' of punctuation is similar, the logic of it and clear scoping are wins to me. You don't end up switching between round and curly braces, and it 'scales' neatly.

1 Like

While I tend to agree with all of these points, I still feel that this is the wrong approach and would like to see other potential solutions - in particular something that either a) preserves the use of commas within the new syntax, or b) allows separation of all and any parameters via newline instead of comma (similar to function builders as mentioned previously).

I'm not really advocating for one or the other, though the second seems less intuitive, even if increasing consistency.

Yes, same rules as in an argument list.

Great point! We should spell this out in the proposal; that's the intended behavior.

Doug

1 Like

After some consideration, +1.

My initial gut reaction to this was "but trailing closures are already kind of confusing...". The naming, IMO, makes all the difference. IMO it's even a clarity improvement to the current trailing closure syntax.

This has a few main advantages:

  1. You no longer lose the information provided by the argument label.
  2. It's much easier to add/remove parameters to functions with optional closure-parameter.
  3. It decreases instances where a wrapper Type is needed.

Example for #1:

// what does the closure do? *shrug*
doAThing(theThing) { 
    moveAlong()
}
//vs
doAThing(theThing) {
    onMove: {
        moveAlong()
    }
}

Example for #2

// adding an optional parameter
doAThing(theThing, before: {
   getUp()
) { 
    moveAlong()
}
//vs 
doAThing(theThing) {
    before { 
        getUp()
    }
    completion: {
        moveAlong()
    }
}

Example for #3

// Instead of having to use a result for a clean API call experience...
makeNetworkCall(body) {
    switch $0 {
    case .success(let result):
    case .failure(let error):
    }
}

// Multiple closers are no longer ugly!
makeNetworkCall(body) {
    success: { }
    failure: { }
}

One question... (how) does this work with variadic parameters?

func runMyFunctions(actions: (() -> ())...)

1 Like

How much do we ascribe those situations to either:

  1. a failure by the library auther to design a "nice" API around the capabilities of the language to feel idiomatic / pleasant to read
  2. on the developer for choosing to elide information they could provide by not using the trailing closure syntax?

At what point do we stop trying to make the language support an API design, rather than push an API design to work within the language?

5 Likes

If it were as simple as API design, I would agree. However it's not currently possible to achieve this syntax:

IMO that's a very clean and expressive way to write a network call. So, at least for me, adding this feature enables good API design. it allows users to interact with your code in a way that works for them while retaining more clarity than before.

3 Likes

What makes it less clean or expressive to write the following?

makeNetworkCall(body,
  success: { ... },
  failure: { ... }
)

The commas?

11 Likes

I do not like this proposal. I don't think it's fixing a significant problem.

Proposed is the syntax

UIView.animate(withDuration: 0.7, delay: 1.0, options: .curveEaseOut) {
  animations: {
    self.view.layoutIfNeeded()
  }
  completion: { finished in
    print("Basket doors opened!")
  }
}

where we can already use the syntax

UIView.animate(withDuration: 0.7, delay: 1.0, options: .curveEaseOut,
  animations: {
    self.view.layoutIfNeeded()
  },
  completion: { finished in
    print("Basket doors opened!")
  }
)

The proposal avers

The mix of ) and } is still jarring to read and surprisingly hard to write correctly

but I think the mix of delimiters is a benefit, not a drawback. A long string of undifferentiated delimiters is worse for by-eye matching than a mixture. (Old Lisp joke: “I've solved the hard AI problem. Don't believe me? Here's the last ten lines of the program:” followed by 800 close parens.)

The single benefit I see in the proposal's syntax is that Xcode's Editor > Structure > Re-Indent command indents the proposed syntax well, and indents the existing syntax poorly. The solution to that is not to change the language. It's for the Xcode team to fix the ding-dang formatter.

14 Likes

Yes.

Moving closures around, for whatever reason, sometimes results in a missing or extra comma where it doesn't actually feel like the comma adds anything.

That, coupled with the switching between round and curly braces, does actually have an impact.