Objections of this kind, seem to me, to fly in the face of the popularity of trailing closure syntax usage in Swift today. Many style guides, including Google's Swift Style Guide go so far as to mandate it:
If a function has a single closure argument and it is the final argument, then it is always called using trailing closure syntax...
From the proposal:
Trailing closure syntax has proven to be very popular, and it's not hard to guess why. Especially when an API is crafted with trailing closure syntax in mind, the call site is easier to read: it is more concise and less nested, without loss of clarity.
The proposed solution to extend "trailing closure syntax to allow additional labeled closures to follow the initial unlabeled closure" aims to extend "the concision and denesting of trailing closure syntax to function calls with multiple closures arguments".
That is, any new API that benefits from this proposal would be more concise and less nested than if it were "called in the standard way, with all arguments inside the parenthesis."
+1. This is a natural extension to the trailing closure mechanism. I think the concern that one of the closures remains unnamed might be valid in the context of some existing APIs, but it will enable and is likely to encourage the design of APIs that are clearer at the point of use going forward. Certainly, SwiftUI and Combine, as mentioned in the examples, would benefit from this extension already. Also, in cases where clarity is an issue, it seems that not using the trailing closure syntax might be appropriate and remains an option.
Yes, absolutely. I'm not sure the animation example illustrates this well, and I think @dabrahams brought up in his comments - the absence of the label on the first closure does not necessarily make its intent clearer. However, it enables API authors to create tailored APIs in which the first closure's intent would be evident from the function name itself. There are already examples of that like Combine's sink.
Yes, I think this is a natural extension of the trailing closure syntax. As far as I can tell there are only 3 choices for extending current trailing closure syntax that potentially fit well with the current feel of the language:
keep all trailing closures named. This seems like not much of a win since it essentially just moves the closing parenthesis before the first trailing closure.
keep all but the last closure named. This, in my opinion, suffers from a significant clarity issue at the call site since the last closure will follow the one before. With this approach, the API author will lose the ability to name their function in a way that allows the reader to naturally deduce the meaning of the last closure.
the proposed approach. This, in my opinion, is the best option of the 3, since it both extrapolates the existing syntax and solves the clarity problem for subsequent closures.
My only real concern here is around the ergonomics of this, especially with regards to code completion, but it sounds like @rintaro already addressed it in his comments.
I studied the proposal itself and I believe I understand it well. I gave the discussion a quick read.
I think that this is the best that you can do with the language today. It's the design that Combine.sink eventually settled on:
ipAddressPublisher
.sink { identity in
self.hostnames.insert(identity.hostname!)
}
ipAddressPublisher
.sink(receiveCompletion: { completion in
// handle error
}) { identity in
self.hostnames.insert(identity.hostname!)
}
However, that's just because the lack of a syntax for multiple trailing closures has left API authors in a bind. When it comes to closures, we've been forced to violate an important API guideline:
Prefer to locate parameters with defaults toward the end of the parameter list. Parameters without defaults are usually more essential to the semantics of a method, and provide a stable initial pattern of use where methods are invoked.
The guideline says methods should have a "a stable initial pattern," but because trailing closures were liable to be unlabeled it was important for clarity to prioritize their stability as the last argument.
This proposal allows us to unify trailing closure syntax with this API guideline
I disagree that UIView.animate is a poor Swift API design. Rather, because it was imported from Objective-C, it respected a more fundamental API guideline that is not limited to Swift—prefer to locate parameters with defaults toward the end—without contorting itself to trailing closure syntax, before it had been properly extended to multiple arguments.
I think you're reading far too much into guidelines like this and are mistaking a desire for consistency for some argument for the inherent superiority of the existing trailing closure syntax. It's generally good, yes, but it leads to some tricky naming problems, as we've been discussing. Additionally, the simple fact that Xcode defaults to trailing closures with the special syntax (when it works, its inconsistency is so annoying) without the ability for users to disable it naturally leads to a very high level of use among the community.
I also think you're making a logical leap in applying the popularity of trailing closure syntax to the wider issue of dealing with many closure parameters. As a few reviewers have mentioned, the actual problem being solved by this proposal hasn't clearly been stated as more than just a desire to apply some form of trailing closure syntax to multiple closures. I think @Nevin's point was that the problem of multiple closure parameters, without the desire for a new trailing syntax, is already solved, and this new syntax doesn't unlock any sort of new APIs.
I’m not too worried about the order. It is easy to overload and provide a different order. The original order could be deprecated and eventually removed. I’d rather see that we are doing the right thing for the future so if we change the order of everything we don’t need to do it again later.
Only because all closure usage assumes trailing closure syntax. If Xcode instead defaulted multiple closures to the enclosed syntax, I don't think it would be nearly as much of an issue.
I think you have this reversed: trailing closures tended to be unlabeled because they were intended to be trailing closures which never used the label, so authors elided it. I think many authors have now cycled the other way, ensuring all parameters have labels and allowing users to use the trailing syntax to elide the last one if they want.
I also wanted to point out that rules like this are often accompanied by a rule stating that, when calling methods with multiple closure parameters, trailing closure syntax should not be used. If this proposal is adopted I would assume we'll see rules in the future mandating always using the trailing syntax or not, not because of some inherent superiority, but because style guides almost always value consistency above all else.
I agree that this is probably a significant factor fueling trailing closure syntax usage.
Both the original and amended proposal have argued that multiple trailing closure syntax can help improve clarity at the point of use. The original did so on the basis of grouping trailing closures together and so distinguishing between arguments (e.g. in SwiftUI configuration and children). The amended proposal also did so on the basis of concision and denesting.
I've also commented in this thread about how multiple trailing closures allow authors to craft APIs more consistent with the API naming guidelines in particular: "Prefer to locate parameters with defaults toward the end" and "Omit needless words".
The proposal also mentions the "rejiggering" required to add an additional closure argument today. Especially if one is following the style guidelines you describe (which I am):
... it's a much shorter journey to go from single trailing closure:
ipAddressPublisher
.sink { identity in
self.hostnames.insert(identity.hostname!)
}
... to the proposed multiple trailing closure:
ipAddressPublisher
.sink { identity in
self.hostnames.insert(identity.hostname!)
} receiveCompletion: { completion in
// handle error
}
... than it is to a parenthesized call site:
ipAddressPublisher
.sink(receiveCompletion: { completion in
// handle error
}, receiveValue: { identity in
self.hostnames.insert(identity.hostname!)
})
This is an extremely narrow use case to justify such a change. I can better see now why this proposal treats the first closure so specially. But I'm not sure this point is generally applicable.
I would also argue, again, that omitting needless words should be left up to the API designers rather than the language's syntax. As we've seen, trailing closures don't necessarily enhance clarity. It's more that they enhance concision at the cost of a small bit of clarity by dropping the external label of the closure. Users learn to use them over time, not because they understood what the closure was doing on its own, but by recognizing what goes in certain trailing closures. This will still be true with the new syntax. There's nothing about the SwiftUI APIs that logically suggests the first closure with no label is the main view builder. If it's used consistently, users will just learn it by rote, not because it makes any sort of logical sense. So while removing control characters can help with readability, none of the costs or benefits here are constant.
Again, that seems like a very edge case to optimize the language syntax around. Not only would you need this syntax change, you'd need a form of the API designed with the new syntax in mind to enable it at all. Is this really that common? It certainly wouldn't be for existing code, so there seems to be little immediate benefit except for those designing new APIs in light of the feature (SwiftUI 2, obviously). And if this sort of evolution (adding previously elided parameters) is really that important, shouldn't it be optimized for all similar APIs, not just those which happen to take advantage of the new syntax? Frankly, it seems like enhancing Xcode's autocomplete to better deal with elided parameters would be a better solution, since it would work everywhere, rather than changing the language for only a particular case.
The design of Combine.sink isn't violating the guideline, it's properly balancing competing concerns. I used the language “prefer” in the guidelines because I recognized that concerns would compete—that's just part of engineering and thus of API design. You can “prefer” to do things one way, but make a choice to do something else because of overriding factors. The guidelines were written to account for those situations, with the knowledge that they couldn't all be anticipated, and with the knowledge that the guidelines were always going to be incomplete.
The root of the word guidelines is “guide;“ they're supposed to provide a framework for thinking about design choices, not an unambiguous prescription for every situation. Treating them as rules that can all be followed all of the time is wrong, as you well know from having worked on SwiftUI: different naming conventions are appropriate in an EDSL, and those conventions aren't covered in that document. Now, I'm not saying ambiguity is good; if I still had the power to edit these guidelines, I'd simply clarify that the consistency of trailing closures takes priority. I think it makes sense too: trailing closures create a much more prominent feature at the use site than which things are defaulted.
This proposal allows us to unify trailing closure syntax with this API guideline
Or we could just resolve the ambiguity in the guidelines as I suggested above.
Is your argument really that UIView.animate is a good API design for Swift because Swift should have been designed differently?
Personally, I think the only fair way to evaluate whether a Swift API design is good is based on the results you get in Swift as it is. Also, I challenge the idea that extending trailing closure syntax to multiple arguments is obviously proper. When we designed Swift syntax we intentionally allowed some flexibilities and disallowed others, creating a careful balance between expressivity, simplicity, consistency, readability, and other factors. Function call syntax, in particular, underwent some really delicate tuning. I'm not saying there's no room for improvement, but overall, I think those choices have worked out extremely well, and am reluctant to open the door to more syntactic flexibility when there appear to be significant downsides.
Finally, I don't at all buy that there's anything fundamental about the defaulted argument guideline. It's certainly completely language-specific, because Objective-C doesn't support defaulted arguments and other languages treat defaulted arguments in ways different to Swift.
Sorry, it appears the main thrust of my argument was lost amid my word choice. Let me try to clarify.
I agree with you that there is an implicit guideline APIs follow:
Prefer to locate parameters with closures toward the end of the parameter list, so they can take advantage of trailing closure syntax.
... that is higher priority than:
Prefer to locate parameters with defaults toward the end of the parameter list....
My intuition is that it follows from this that the order of parameters should be:
Non-closure parameters without defaults.
Non-closure parameters with defaults.
Closure parameters without defaults.
Closure parameters with defaults.
When I write APIs today I am forced to swap (3) and (4), which I feel is a contortion due to the limitations of trailing closure syntax. I think multiple trailing closure syntax adds value, in part, because it facilitates synergy between the implicit trailing closure and explicit default parameter API guidelines.
It's been said many times and many ways in this thread, but I do feel like we wouldn’t be here at all if Xcode didn’t do this and/or had better indentation when autocompleting multiple closure signatures.
I'm much more in favor of the amended proposal than the original version, and would be ambivalent-to-happy for it to be merge. But, to Dave’s point, we’ve spilled about as much ink on this pitch and review as on the extremely cursed swift-format thread, for something that doesn’t really change the face of the language.
Yeah, there’s an implicit guideline, but that ain’t it. The guideline that’s implicit in the language design is:
prefer to locate a parameter with function type at the end of the parameter list, and if there are multiple such parameters, prefer to put the primary one there.
Having guidelines match your intuition is nice (though subjective—I have a different intuition about the implicit guideline here) but should take a back seat to good results at the call site. I don’t believe that burying the primary closure in the middle of a function call, which is what this proposal and its associated change of guideline do, produces a good result.
My issue with Xcode formatting has to do with the way it indents the various bits of closures, and has nothing to do with inline versus trailing closures. In fact, I often avoid trailing-closure syntax if I'm chaining calls because Xcode's indentation is absolutely horrid.
P.S. I tried to create some examples, and it seems that the current version of Xcode has improved in this area. I'm happy to see it.
I can understand being frustrated with a tool that you use and it not working the way you'd expect but calling people "Amateurs" is not really helping your point. Writing a formatter that handles each and every situation in a consistent and an aesthetically pleasing way is an enormous challenge. For example, Bob Nystrom -- author of Crafting Interpreters and one of the members on the Dart team -- has written about this in his blog post The Hardest Program I've ever written.
Please try to keep the discussion focused on feedback and constructive criticism for the proposal that is under review.
I positively think my feedback its quite constructive, even if it lacks nuance and reveals a great deal of frustration. The underlying problem is a fundamental organizational issue: pretending such a proposal can be designed in thin air, without any consideration for the tooling, is the sign of a huge blind spot. The fact that no quality assurance stage is even thinkable is utterly concerning.
In short: please stop playing with the language syntax until a QA team can do its job. The project has reached a level of complexity that prevents such a proposal from providing a good level of confidence in the quality of the outcome. We need tools. IMHO the formatter project should help a lot on this matter, but I'm just a QA amateur.