+1
Who cares if it’s “sugar”? Clarity trumps academic purity.
Well, sugar needs to be sweet. It won’t be good if it barely taste any different.
I could quibble with #3, because I'm not strongly motivated to reduce the nesting; rather, I prefer for the closures to be separated because they're a different kind of parameter (an action to take) and they are often more than one line. That said, I understand the desire to reduce nesting, as expressed by a number of posters here.
I can see the appeal of this. It eliminates the extra curly braces, and with the style you're suggesting it also eliminates the nesting and provides a more control-flow-ish feel. @xwu brought this up earlier in the thread. It still looks a little odd to me, I think because I'm more strongly influenced by get/set in computed properties and subscripts than by control flow, but I can certainly live with it.
Well, both solutions solve this issue, and that should be called out in the proposal. I agree that the "delta" to fix the ambiguity is less with your preferred syntax.
Doug
When I realized that anything, anything, can be parenthesized, @Chris_Lattner3’s suggestion upthread becomes much more appealing. I think it is a solid alternative.
Personally, I find some of the value of trailing closures (both as they exist today and in the proposal under review) to be that the "callback" code by separated from the other, non-closure parameters. Your proposed syntax for a different way to specify parameters mixes the non-closure and closure parameters back together, which I find less clear.
To me, it also feels like it's optimizing for the DSL case a bit much, and packaging all of the arguments into the comma-less '{}' environment has all of the down-sides of the rejected SE-0257.
Doug
I see merit in putting closures in a special basket since they flow differently and all that. Though I still hope that this proposal will also sweep up the expressions. Given how easy chaining is, you’ll end up with long ones pretty often.
Also, putting closures after all non-closures would mitigate the confusion regardless of syntaxes. Conversely, mixing the order of closures and non-closures would be confusing in any syntaxes.
One other thing, does the proposed syntax require newlines and parentheses. Would this work?
foo {
bar1: { ... } bar2: { ... }
}
Not all API guidelines are about visual style. This guideline--which I've cited from three distinct sources and heard about from several more---is an indication of a language deficiency. The convention described in the guideline is workaround around that deficiency, and that's about as strong a signal as you're going to get that a syntax is causing "actual harm."
It is counterproductive. Tossing around the word "charade" belittles the effort of everyone participating in the proposal process. That is not how we want this community to operate.
This proposal attempts to solve a problem. Many people have agreed that the problem is real, even if they don't like the solution being proposed. Or maybe it's a problem that shouldn't be solved. That's why we're here---to find the best solution for the language, and it's very likely to be in this thread somewhere.
Feel free to argue either way---toward a better solution or no solution---and we'll better off having had the discussion, but do not assume that others are operating in bad faith.
Doug
I think this is a really important point. Consider the proposal and how it intersects with the existing classical trailing closures. It is basically arguing that instead of writing:
// Non-trailing closure syntax.
foo(a: 1, b: 2, c: {
stuff
})
// Existing trailing closure syntax.
foo(a: 1, b: 2) {
stuff
}
That we should use something like this:
// Proposed trailing closure syntax.
foo(a: 1, b: 2) {
c: {
stuff
}
}
I can't see any way at all that this is a progression over the non-trailing-closure syntax, and it isn't providing near the value of the existing trailing closure syntax. I don't see it as increasing clarity at all: it is just "moving things around" as many people on this thread have observed.
Amusingly, I'll observe that this is very similar to Objective C message sends without a receiver. If you took the maximally crazy version of what I wrote above, then these would be equivalent:
foo(a: 42, arg: bar(b: 19, c: "string", d: thing), other: 93)
(foo a: 42 arg: (bar b: 19 c: "string" d: thing) other: 93)
The only major difference I see w.r.t. objective c selectors is that you could leave off the outer level of parens.
I think the major argument against this is (as multiple people have brought up) that this approach provides a completely new and different way to write Swift expressions. I think that this is a very serious issue, and we should discuss whether this matters, and if so how to reduce the damage caused by unnecessary divergence in Swift code in practice.
I am personally a superfan of allowing authors of important libraries (e.g. SwiftUI) to take significant control over user-exposed syntax when working with their libraries. However, it is also really important to me that libraries from different groups compose correctly, and I do not find it appealing to provide multiple nearly-equivalent ways to do things in general. In my opinion, this argues for this behavior to be limited to APIs that opt-in to it with an attribute. This is similar to how the existing Function Builder feature is only applied to APIs that are specifically designed for it - it is not a sweeping change to all code everywhere.
Yeah good point, it is worth adding that note to the proposal, but this is also not a particularly big deal in practice.
-Chris
I read the proposal and felt that the examples offered present a really strong case for adoption. I did not read the comments.
waiting for wwdc apple guys to tell us why to change like this,closure of labeled closure!
Every argument but one: the {
and }
in trailing closures today only delimit multiple statements, not multiple parameters.
With a trailing closure today we have:
array.filter { // statement delimiter
... // statements
} // statement delimiter
With this proposal we would have:
Button { // parameter delimiter
action: { // statement delimiter
... // statements
} // statement delimiter
label: { // statement delimiter
... // statements
} // statement delimiter
} // parameter delimiter
This is the added conceptual complexity I’m talking about.
(I’m not saying that the existing trailing closures don’t also add conceptual complexity of the language. They certainly do! My point is only that this proposal would add even more conceptual complexity.)
I can certainly feel that angst sometimes! Even though I use trailing closures liberally otherwise, I sometimes write array.contains(where: { ... })
instead of array.contains { ... }
when I think omitting the label hurts readability. But array.contains { where: { ... }}
isn't really better, is it?
Is there a way this could be implemented as a data structure that is declared as part of a function signature?
A lot of examples here have used trailing non-closure arguments and other unconventional approaches (e.g. variadic trailing closures) that have demonstrated the difficulty of implementing this while keeping things unambiguous with existing trailing syntax and function builders.
If we had a separate data structure for defining trailing closures then we could
a) eliminate the use case of having a trailing non-closure argument amongst existing closure arguments by constraining the use of the structure to final argument of a function only
b) make it a requirement of the function declaration to specify that multiple trailing closures are desired/required.
Other considerations such as optionality / omission of a given closure could then be bundled into that data structure.
Example (syntax for demonstration only):
typealias Completions<Output, Failure> = @[
completion: @escaping (Subscribers.Completion<Failure>) -> Void,
value: @escaping ((Output) -> Void)) -> AnyCancellable
]
// ...
func sink(completions: Completions<Output, Failure>)
// ...
publisher.sink {
completion: { ... },
value: { ... }
}
This is off the top of my head so may cause other issues, but I feel that the explicitness is more in keeping with Swift and would make patterns such as function composition far simpler.
(Apologies if this has already been suggested, this thread has moved very quickly).
EDIT: to clarify, this would make a multi-closure data structure similar to a dictionary in that they would be declarable as a destructured type in function signatures, and as value types with their own discreet syntax when passed as arguments.
I think I like the counter-proposal of (1) allowing trailing closures to have labels and (2) allowing multiple trailing closures as long as they all have labels. That fits in much more cleanly with the existing language.
The interesting thing about this idea is that it actually strengthens my resolve about overloading the meaning of curly brackets being a bad idea for this use-case. Looking at the example here, I find myself asking “why do curly braces get a special meaning for the Completions
type?”
The same category of question doesn’t apply to the existing trailing closures syntax both because It isn’t up for discussion (directly) in this proposal and also because it doesn’t overload the meaning of any delimiters. It shifts delimiters, and for that reason it changes the intuitive flow of the code to mirror control flow in a way that is often desirable.
My example was only to demonstrate how a group of related closures could be formalised as a single data structure, not to give an opinion on the syntax under discussion.
In this case the Completions
type is just a typealias for a context-specific declaration of this new type of structure. If you were to typealias a closure as MyCompletion
you might just as well ask "why does MyCompletion
get special trailing syntax?"
As with closures, the type could be just as easily inlined into the function definition, as we currently do with tuples and dictionaries ... though I personally prefer to always typealias a tuple rather than inline it's definition. Regardless, the option is there and would be consistent with other language features.
With regards to my opinion on the choice and use of various delimiters, I kept them as per the proposal in my example code for ease or reading but, as an aside, I do agree with you:
Since @Douglas_Gregor cited Google's Swift style guide above, which I maintain, I should point out the internal version of our guide added a third option for when using non-trailing-closure syntax is permitted, and it's around this issue. In addition to the two options listed here, we now also allow:
- If the closure's label conveys non-obvious information.
(Note to self: I really need to bring the external guide up-to-date with recent internal changes.)
This ties into your point: someone could argue that the context provided by the label of contains(where:)
is non-obvious and omitting it is confusing, at least if you're a newcomer to using the API—is it a predicate, a completion block, something else? So we would allow this:
let wasItThere = array.contains(where: { $0.someProperty })
This proposal would allow this to be written exactly the same but with the parentheses swapped for braces. This, I feel, provides no real benefit, and is actually a negative.
However, I think I really like how the alternative discussed above would look:
let wasItThere = array.contains where: { $0.someProperty }
That just feels cleaner. Admittedly it's a small change—it's only removing parentheses—but instead of viewing it that way, I think it's better to view it as adding context back to a trailing closure without introducing any additional noise to the syntax.
And if people think that the where:
label in contains(where:)
isn't non-obvious enough, here's a much better example:
// without trailing closure
transitioningController.animate(alongsideTransition: { _ in
self.view.alpha = 1
})
// before, with trailing closure and no label
// what is this? a completion block?
transitioningController.animate { _ in
self.view.alpha = 1
}
// after, with trailing closure label
transitioningController.animate alongsideTransition: { _ in
self.view.alpha = 1
}
How about remove :
?
Might be more control-flow-like
UIView.animate(withDuration: 0.7, delay: 1.0, options: .curveEaseOut) {
animations {
self.view.layoutIfNeeded()
}
completion { finished in
print("Basket doors opened!")
}
}
And if remove new-line even more looks like control-flow
UIView.animate(withDuration: 0.7, delay: 1.0, options: .curveEaseOut) {
animations {
self.view.layoutIfNeeded()
} completion { finished in
print("Basket doors opened!")
}
}
It was discussed, that it would be ambiguous with function calls.
Oh... Okay. That's sad.
What do you think of array.contains(where:) { $0.someProperty }
, which already works in the language? Is dropping those parenthesis really that significant?
Obviously, a lot of this comes down to individual aesthetic preference but I find array.contains where: {
really weird and not befitting of the language.