This review is probably going to seem very negative. Sorry for that, but I think the proposal is fundamentally flawed and it wouldn't serve the language to pull punches here. -1 on accepting it.
Problems With The Problem
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.
Second, the description of the problem is uncompelling:
// Without trailing closure:
UIView.animate(withDuration: 0.3, animations: {
self.view.alpha = 0
}, completion: { _ in
self.view.removeFromSuperview()
})
// With trailing closure
UIView.animate(withDuration: 0.3, animations: {
self.view.alpha = 0
}) { _ in
self.view.removeFromSuperview()
}
In this case, the trailing closure syntax is harder to read : the role of the trailing closure is unclear, the first closure remains nested, and something about the asymmetry is unsettling.
Handling the claims in reverse order,
- “Something about the asymmetry is unsettling” — just the sort of subjective assertion that makes me deeply suspicious that changes are being driven by criteria that leave us with no identifiable guiding principles. If we start making adjustments to the language whenever “something is unsettling,” different examples and peoples' experience will dictate contradictory choices and we'll be adjusting forever.
- “The first closure remains nested” — so what? That's not a problem in and of itself.
- “The role of the trailing closure is unclear” — yes. That's a problem to be solved.
Overall, the readability of the examples is harmed by formatting that hides the structure of the code (and a needless with
in the argument label—but never mind that), which makes it hard to evaluate the real issues. Once reformatted:
UIView.animate(
withDuration: 0.3,
animations: { self.view.alpha = 0 }
) { _ in self.view.removeFromSuperview() }
I can see the problem quite clearly; it's about the role of the final closure. Looking back at the proposal's example of successful trailing closure use, we find:
UIView.animate(withDuration: 0.3) {
self.view.alpha = 0
}
What makes it OK that the trailing closure is unlabeled in this case? For me, it's fine because it represents the change that's being animated, in other words, it has a primary role in the call that can be deduced from context—and I see that the proposal identifies “primariness” as the reason trailing closure syntax usually works out. The issue with the “problem” example is that we've put a closure with a secondary (and inconsistent!) role in the primary position at the end of the call. If the example with one closure is good, I suggest this should also be considered good:
UIView.animate(
withDuration: 0.3,
completion: { _ in self.view.removeFromSuperview() }
) {
self.view.alpha = 0
}
And if you're going to argue that the role of the trailing closure isn't clear enough in this example, I'd ask that you explain why it's clear enough in the example with a single trailing closure, and not here.
To sum up, the one example cited as a problem suffers from poor Swift API design, because it wasn't designed for Swift—it was imported from Objective-C. Before we complicate the language with new wrinkles on calling syntax we should have—not just one, not just a few, but many— examples where there are problems that couldn't have been solved with better API design choices.
Problems With the Solution
I have nothing in principle against extending the expressiveness of the language by making it possible to label trailing closures, nor with features that reduce nesting. This proposal, however, moves the de-facto position of the primary closure from the end to the beginning of the parameter list. Existing multiple-closure APIs that were sensibly designed to take advantage of the status quo now have an even terser syntax that puts a label on the trailing closure. So, for example, if the closure order in UIView.animate
had been well-chosen, users could write their call as in my last example, or they could write:
UIView.animate(withDuration: 0.3) { _ in
self.view.removeFromSuperview()
} animations: { self.view.alpha = 0 }
It seems to me that the above is no better than the original when it comes to clarity, at least if you buy the idea that the problem is that the secondary closure needs a label.
By adding more options for how to call a given function, we are taking control away from API authors over how their APIs look in use, and transferring it toward users. That's not necessarily wrong; API authors can't control how users will format their code or name their variables, both of which can have a huge impact on how well an API's use site reads; clients have to participate in making use sites read well.
If we're going to take this tack, though, let's start with a simple change that addresses the only real problem raised here: sometimes you need a label for the trailing closure in order to make its role clear, and to use it you have to give up on the de-nesting benefits of trailing closure syntax. Just allow the existing trailing closure syntax to be optionally labelled for clarity.
Problem of Focus
This proposal seems to throw a relatively large change at a very small, syntactic problem, as far as I can tell. IMO there are enough real fundamental problems to solve in Swift (ownership, CoW slice, variadic generics, concurrency, … off the top of my head) that if we're going to “do syntax sugar,” we should keep the changes small and conservative, and we should be rigorous with ourselves about the problems actually being solved, so we can put attention on the fundamentals. This proposal seems to do neither.