-
Single closure : label can be omitted
optional
-
Multiple closures : labels must be added include
_
required
-
What is your evaluation of the proposal?
Very heavy -π [-1 for seachability].
-
Is the problem being addressed significant enough to warrant a change to Swift?
I think not. In my mind, the problem already has a solution. Just put all your closures in the parameter list with the rest of your arguments. It's not really a concern of character count (we aren't code-golfing for the most part), it's just an additional comma for each closure to separate the method's arguments. You get the parameter labels, so there is no confusion about what the closures do (unless you have bad API design, but that's a different discussion), and you get the closing parenthesis after the final closure argument to keep everything nicely grouped together and easy for the eye to parse (granted, that's probably an opinion of mine and will be debated by some).
To be perfectly honest, I would prefer completely removing trailing-closure syntax from Swift rather than have this. It would help alleviate the issues that can occur with bad API design. I've personally found myself confused by the result of trailing closures at times, especially in initializers or when the closure is the only argument of a method (or combining the two
):
let handler = EventHandler { event in print(event) }
-
Does this proposal fit well with the feel and direction of Swift?
I actually need to answer this point in two parts:
-
Feel: No. What this gives us is a floating identifier followed by a colon after a trailing closure. Does that identifier have a keyword before it declaring what the identifier is for? No. Is there a type definition after the colon? No. Is it a scope label? Yeah, that's probably the closest thing. So now I can break out of the closure? Sorry, nope, that's not what this is. Being able to overload the
.
operator would make just as much sense to me. -
Direction. Sadly, yes. Since the dawn of the SwiftUI era (and don't get me wrong, I love SwiftUI), we have seen a plethora of proposals adding syntax sugar that causes more confusion than conciseness. Granted, I think property wrappers are amazing (if used correctly), but some like function builders and the optionality of
return
statements are incredibly finicky and make it really easy to create code the is hard to follow. The funny part is, I think that the SwifyUI example in the proposal is actually the hardest one to parse, and once you add that in with the vast amounts of nesting in brackets that it can take to create a view,youI won't be able to see the program for the code.
Anyway, the point is that this is another proposal that follows that pattern of trying to add sugar to something that is plenty sweet already. Add some salt for once.
-
-
If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
N/A. I have not seen similar features in other languages I have worked with.
-
How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
I've loosely followed the proposal on the forums and read through the proposal while I was writing this response.
Actually, I think UIView.animate
is one of the few APIs that would immediately benefit from this proposal, due to its first closure being its primary action. Of course, most other APIs will not get that benefit and will have a poor interaction with this feature by default. And like you say, the bigger improvement would be to make completion handlers unnecessary.
-1
No substantial improvement but increases complexity.
This feature would allow calls to UIView.animate
to be written slightly differently, sure - it's introducing a different syntax, after all. I wouldn't say that this alternative provides any "benefit", though.
There are certainly issues with the existing API, but this feature doesn't make them any better. It doesn't make the pyramid of doom any less prevalent or easier to read, it just adds more labels in the pyramid.
Another thing: I posited that completion handlers make up the vast majority of uses of multiple closures. I don't have data to back that up, unfortunately (I'm not sure how I would mine that information from GitHub), but actually, the fact that async/await even exists kind of supports the argument by itself. Why would so many languages add special syntax to flatten those "continuation" closures if it wasn't an extremely common pattern?
A lot of the arguments for this proposal seem to be based on assertions without evidence/intuition. If this was the 1970s, that would be fine, but this is 2020 and these days we make decisions based on data. I have yet to see data indicating that this is even a problem, let alone that developers prefer this syntax, and I think this shows a lack of care and consideration when compared to other aspects of the language.
It seems like these days, Swift is a toy to allow Apple to experiment with exotic syntax and rush it in to production. I think the core team, and in general the users on these forums (who are some of the most knowledgeable Swift developers in the world), are underestimating the impact this additional complexity will have for everybody who is not us.
+0.5
For me trailing closures were always among the best looking and most compelling syntactic features Swift had to offer. They fit the language because they are IMHO straightforward and easy to understand.
Therefore I think that it is a no-brainer, to extend this syntactic sugar, where it's usability is limited. I agree with the arguments presented in the proposal that this is e.g. the case, when there are multiple (possibly defaulted) closure parameters at the end of a function.
I think that the proposed syntax builds really well on top of our current trailing closures and that the simplicity is perfectly maintained, at least in the presented examples.
As many others also mentioned in this thread, I can think of some use cases though, where the API design would really benefit from the option to explicitly state the argument name of the first trailing closure too. This would be especially useful for functions, which take multiple equally important closures. Binding(get:set:)
mentioned by @Lantua is probably the most prominent member of this group of functions.
APIs designers should even get the possibility to obligate the explicit first trailing closure argument name IMHO, because I believe as many others, that the caller of the API should not necessarily have to decide for or against it by himself.
Something like an opt-in function annotation like @michelf suggested here should be considered (obviously the best approach would be to require the argument name, if the label wasn't underscored in the function declaration, unfortunately that would be source breaking).
I would definitely be +1 for this proposal, if it became possible, to label all trailing closures and to require such use in an API. It would also make the Renaming Trailing-closure Functions in the Standard Library pitch somewhat obsolete, which I would rather appreciate.
In my eyes yes, especially if labelling all trailing closures became a thing.
Definitely. IMHO it extends the current trailing closure syntax very naturally and also follows the current direction of Swift, by allowing e.g. better DSL-APIs.
N/A
I read the proposal and the whole second review thread.
I found your choice of ()
in the second example interesting. Using an optional first argument label could also prefer this parenthesized style to avoid the juxtaposition issue mentioned in the proposal.
To be honest this syntax:
myArray.filter() where: { ... }
was unintentional. But as you mentioned there are some benefits. The parenthesis differentiate between properties and methods, which I think is quite good for a beginner.
Of course, this syntax would add more visual clutter and create inconsistencies between a no-label and a labeled first-closure approach. Also, to be fair there is (currently) no feature in swift that allows for labels or keywords to be next to properties (except for operators). As such, these are clearly method calls (after getting used to the following syntax):
foo.bar { ... }
myArray.filter where: { ... }
What's good for beginners is good for everyone. Functions, methods and initializers should IMO always have parentheses for clarity. Omitting them should be reserved for the rare cases where the function acts like a language keyword.
I agree. Additionally, any reasonable editor would color or style labels differently to make it absolutely clear. A newline could also help make the juxtaposition clear (particularly in DSLs). The proposal seems to dismiss this because in their opinion the juxtaposition is not clear enough or jarring due to lack of special characters. I think the ability to either add ()
or ability to drop the label should provide alternatives if juxtaposition is not clear enough or jarring in some contexts. With an async syntax potentially coming out that will make completion handlers less common, I think multiple trailing closures will mostly be used in DSL contexts. I think it would be nice to allow as much flexibility as possible here. Particularly since allowing a first closure label is more consistent with normal arguments. I hope this feature ends up coming in later if the proposal is adopted without them.
I do think filter is kind of a bad example though. It is common enough that I don't think it needs a really clear label. I think an exception for normal verbosity is fine for a handful of functional programming inspired functions. We should just know all of these by heart. However, I think the ability to add an optional label is great for those learning the language.
Many of my thoughts are written out in more detail in Renaming Trailing-closure Functions in the Standard Library - #7 by allevato and later in that thread, and I've tried to summarize them below.
I still feel strongly that the syntax in the current revision of the proposal is a vast improvement over the original version. However, my advocacy for this solution in the first review was also predicated on the idea that it would allow the first (or only) trailing closure's label to be written out, which would remove the syntactic ambiguity when using a function call with a trailing closure in a conditional statement. However, that wasn't done here; by omitting that possibility, the syntax misses out on a possible solid improvement to the language.
The issue is simply this: Swift's trailing closure syntax was designed with single trailing closures in mind. If we want API authors to now write functions that use multiple closures and use trailing closure syntax, we need a holistic solution that unifies trailing closure syntax everywhere in the language. Grafting new bits of syntax to the existing system with its pre-existing exceptions is just going to make the language more complex and confusing.
If I could design what I think are ideal rules for trailing closures from the ground up, they'd be something like this:
- All closure arguments in a function call which are not followed by a non-closure argument can be written using trailing closure syntax. Multiple trailing closures would be expressed through juxtaposition, as proposed here.
- If there are no arguments preceding the trailing closure(s), the empty parentheses may be omitted (as they can be today).
- If a trailing closure argument is declared with a label, it must be specified, regardless of its position (i.e., including the first).
- If a trailing closure argument omits its label in its declaration (uses
_
), then the first such closure at the call site omits the label entirely; all subsequent closures must be labeled_:
.
This would make trailing closure argument labels act almost like all other argument labels. Sadly it doesn't fix all cases of the conditional statement problem—if a function's first closure is unlabeled, it'll still be ambiguous, but in that case, surrounding it by parentheses is probably less ugly than other alternatives I can think of (like allowing _:
to be in the first position), and it still solves the problem for labeled closures, of which there are many.
Allowing the first trailing closure to have a label would also fix a major problem for API designers: APIs designed to be used with trailing closures cannot provide any meaningful information through a label about what that closure does. Is it a completion callback? Is it a transformation function? Is it something else entirely? You have to guess from the context, and sometimes it's not obvious. The language's choice to unconditionally elide the label removes the ability of the API designer to make that label meaningful. The thread linked above points this out for a handful of standard library functions that take a single closure argument by proposing to rename the functions to graft the label onto the basename, but IMO that makes those functions look worse, and it does nothing to solve the problem for functions that have arguments preceding the closure, where there is no place you can graft the label onto.
Unfortunately, what I don't have is a great idea of how to balance these concerns with the source compatibility requirement.
Overall, however, I do think this proposal is syntactically a huge improvement over the first iteration, and that this would be a good improvement for emerging multi-closure APIs. But I think some of the details still need to be tweaked. As some folks have mentioned upthread, allowing the first trailing closure's label to be optionally specified (even when there is only one trailing closure) would resolve some of the syntactic ambiguities of conditional statements, and it would give API designers the ability to make that label meaningful again. Then, perhaps a future language mode could start making the first label mandatory as well.
If trailing closures were redesigned from the ground up, I somewhat prefer this syntax for the unlabeled single closure so that it could work in conditional statements. I can't think of any other syntax that feels like argument labels yet would also be non-ambiguous in general cases. Obviously this would require changing how loop labels are specified and break a lot of existing code. I agree with everything else in the if-we-could-start-over design.
myArray.filter: { ... }
+1. This states much more clearly the direction I alluded to here: SE-0279: Multiple Trailing Closures [Amended] - #43 by anandabits. and here SE-0279: Multiple Trailing Closures [Amended] - #48 by anandabits.
I think the alternatives are demonstrably worse. I don't think there's a way to solve this problem well in both the language and standard library without incurring source breakage of one kind or another. So I'm in favor of accepting the source breakage this approach would incur. If the current syntax can be deprecated before it is removed altogether, even better - projects can migrate at their own pace.
We've seen a lot of examples with clarity problems in this thread, but not all of them are related to trailing closures or argument labels, and IMO if we're going to make a change to the language, it's really important to be clear about which problems it solves. As I have said before, the core problem with filter
is that we don't know the polarity of the predicate, and seeing where:
does nothing whatsoever to help with that. The only cure for making these calls clearer at the call site is to get that information into the name, e.g.
a.selecting { x in x % 2 == 0 }
I don't think a limitation stemming from the compiler is the right tactic. In previous discussion regarding whether white space rules should be enforced by the compiler or not, most agreed that flexibility from the compiler would be best. I believe the same should apply here. I agree that names of methods with closure arguments should be carefully considered; the example with selecting
demonstrates that quite well. Nonetheless, giving API designers the flexibility to optionally add a label is, at least as I see it, more aligned with the design philosophy of swift.
Besides, optionally specifying a label for the first closure would be more consistent with the proposed syntax and would be easier for beginners to grasp. With @allevato's (which is, in my opinion, the most refined up to this point) proposal the only odd thing to a beginner would likely be the fact that the label of the first unlabelled trailing closure (in a function call) can be omitted, whereas other labels can't:
func foo(_ bar: () -> Void, _ baz: () -> Void) { ... }
foo { // label was omitted
...
} _: { // label can't be omitted
...
}
That's a rule I find perfectly logical, as with the lack of _:
would make the fact that this is function call unclear.
What is your evaluation of the proposal?
-1.
The premise of the proposal is a subjective preference for expressing multiple closure arguments in some form of trailing closure syntaxes. The core rationale, as I understand it, is the author's opinion how the use of trailing closure in a call site with multiple closure arguments causes confusions and legibility issues, due to the unsettling asymmetry.
While I do not entirely disagree, the proposal does not explain:
-
Why should using multiple trailing closures be the language's terse solution for function calling with multiple closure arguments? Do we need a terse syntax?
-
What are the pros and cons, regarding e.g. expressivity and information hierarchy, in introducing new grammar and syntaxes into the language?
Taking the prime example in the proposal & the discussions, today's language and tools can sufficiently express and format it in today's syntaxes:
UIView.animate( withDuration: 0.3, animate: { self.alpha = 0.0 }, completion: { _ in self.removeFromSuperview() } ) Binding( get: { self.entity.isEnabled }, set: { self.entity.isEnabled = $0 } )
In my opinion, this already has a well-defined and natural information hierarchy, and carrying the same amount of information as the proposed syntax. So it is very hard for me to believe that the language needs new syntaxes, even if we put aside the controversies around the bundled rules.
-
Quoting the proposal:
As a result, if we ever need to append an additional closure argument to a function, many of us find ourselves having to rejigger our code more than may seem necessary [...]
What is the thinking behind the issue that it must be resolved by expanding the Swift language grammar with an form of "chained labelled curly brackets" calling syntax, IMO very unfamiliar in C/Java family, or even among Swift-like languages with closure/lambda support?
Is it impractical to trackle this as a refactoring problem in IDEs, formatter or linter?
Is the problem being addressed significant enough to warrant a change to Swift?
Does this proposal fit well with the feel and direction of Swift?
Unconvinced.
Agreed. I would prefer a full holistic change, even if it is source incompatible or no change at all.
I don't understand what problem is being addressed by this holistic “solution.” Swift today supports passing multiple closures and using trailing closure syntax. Only one closure can be “trailing;” it's the one at the end of the argument list. The other ones don't trail, so they don't use trailing closure syntax. This is by design, at least for some of us who were involved in the design.
I'd like to elaborate just a little more on why the current design is good and why the proposal is bad for the language, then I'll be done here.
Trailing closure syntax leverages “code shape recognition” instincts formed by the precedent of common built-in constructs like for
loops, where the body, which is part of the loop, “hangs off the end,” of the construct rather than being nested inside it. That position at the end is a recognizable, uncluttered, place of privilege; it signals to the reader that it's the main part, and if there are braces in other parts of the construct, they tend to get nested inside parentheses:
for x in (a.lazy.map { $0.aspect }) {
theBody()
}
(a.lazy.map { $0.aspect }).forEach {
theBody()
}
Extensible constructs like if
/else
and do
/catch
, with multiple braced parts, are recognized as a chain of these little patterns, rather than as one big unit, so are analogous to chained method calls:
if_(someCondition) {
clause1()
}
.elseif_(nextCondition) {
clause2()
}
.else_ {
clause3()
}
Sometimes the conditions/patterns in these constructs involve secondary expressions with braces. As with for
, these secondary expressions don't move to the end of the unit just because they involve braces:
if (a.contains { $0 < 0 }) {
clause1()
}
else if (a.contains { $0 % 2 == 0 }) {
clause2()
}
else {
clause3()
}
The one exception to this rule is repeat
/while
, which is extremely rare in practice and in my experience reads very poorly when the condition involves a closure, because it clashes with expectations set by the rest of the language.
These proposals suggest that the main/body part of function calls that take multiple closures should no longer be at the end, but somewhere in the middle. There are at least two problems with that:
- That position is inconsistent with the precedent set by language features, which will make it harder to recognize patterns in the code.
- Independently of any precedent, that position, “buried” in the interior of the function call expression, with a label and another closure following it, is objectively less prominent.
I think, this would be the best solution and it could be done without breaking sources, if the label for the first trailing closure was optional at the beginning. The compiler could spit out a warning, that in the future the label will be mandatory, which could be introduced in Swift 6 in some special syntax mode.
Whoa! I fell asleep, and woke up to find this proposal headed in a source breaking direction.
Unless I am completely misunderstanding, the benefit to be derived from adopting the proposal is in the nature of improved formatting. Correct?
If so, I have a very difficult time balancing that benefit against the major downside of breaking source compatibility. What am I missing?
Apologies in advance for making such an ill-informed post. I’d like to think that, in this case, distance from the details provides useful perspective.