SE-0279: Multiple Trailing Closures [Amended]

I haven't done a deep review, but I agree with Jordan and others who say that we shouldn't provide special behavior to the first closure parameter just because it was first. All closure should have to respect the labels provided by the API author.

Now, the proposal provides parsing and grammar rules for the compiler to be able to parse peculiar expressions of this logic, but that doesn't help users at all, especially when the parameters are unlabeled or defaulted.

func pointFromClosures(x: () -> Int = { 0 },
                       _ y: () -> Int = { 0 },
                       _ z: () -> Int = { 0 }) -> (Int, Int, Int) {
    (x(), y(), z())
}

Now what does this mean:

pointFromClosures { 1 }

If you're familiar with the peculiar rules of this new multiple closure behavior, you'd know that it's x getting the value there. However, if you have any previous Swift experience, you might assume it's z, as the trailing closure, that gets the value. I think this is a problem: this new syntax is incompatible with current Swift behavior while appearing the same.

Another problem:

pointFromClosures
    _: { 2 }

Which closure gets the value 2, y or z? If you following the parsing rules laid out in the proposal you'd know it was y, but most users won't know that. So with these new complex rules, there's little the user can do to intuit a correct result.

And a final issue:

pointFromClosures {
  1
} _: {
  2
} _: {
  3
}

This just looks bad. I think full excision of labels looks better.

pointFromClosures
{ 1 } 
{ 2 }
{ 3 }

Ultimately, though, much of the issue here was just bad API design, so lets assume we fully labeled the closures:

pointFromClosures
{ 1 } 
y: { 2 }
z: { 3 }

Like @anandabits said, this looks odd as well, since the values are peers and should have the same labeling requirements.

Finally, I take issue with this text in the "Alternatives Proposed" section:

While this syntax is clear at the point of use, pleasant to read, and provides contextual cues by separating the trailing closures from the rest of the arguments, it risks evolving into an alternative calling syntax. The proposed syntax is more concise and less nested, without loss of clarity:

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

We have a new topic talking about this precise issue, so I don't think you can argue there's no loss of clarity: you'd have to understand what the first closure does in the animate example just like in the count example from the other topic. It's been brought up earlier in this topic, but I found the responses to be less than satisfying.

Overall, I'm not sure this proposal goes far enough to distinguish between closures of relative importance, fully solves the usability issues around multiple trailing closures for typical users, or gives API developers the tools necessary to guide such users toward the intended call sites.

16 Likes