Support use of an optional label for the first trailing closure

Thank you all for a wonderful discussion—and for staying for the most part away from rehashing SE-0279. Many will agree, I hope, that it’s been just as important to prove to ourselves that we can engage with this topic in a productive and dignified way as it has been to tackle the technical details themselves.

I think we’ve captured many well articulated sentiments of the community in this thread (not to mention all the ink spilled before). I’ve gently augmented the text to include all the interesting arguments elucidated here about caller appearance and created a PR for the proposal, with the hope that the core team will proceed to review soon!

26 Likes

"Treat warnings as errors" being turned on seems like standard practice for projects I work on these days that run a tight ship.

I'm not sure library authors should be deciding what syntax I as a client of the library get to use. Recommending (== making it a warning) seems fine, requiring (== an error) feels a bit heavy handed.

Just my twenty cents (inflation! :) ).

1 Like

Thank you very much for putting this together. Clearly stated and very helpful.

The fact that labels may be elided in trailing closures is an anomaly that (based on the recent discussions) is widely considered a mistake. We don't allow users to choose to elide any other labels a library author has chosen. I don't think this flexibility would be accepted today if it wasn't already a part of the language.

9 Likes

Thanks. I guess the kinds of places where eliding the labels makes sense and really does seem to improve readability are places that library authors tend to make them optional in the declaration. Good enough. I definitely support this proposal as written, was just not so sure about your push to make it an error.

I hope while we're considering updating these rules we'll also consider how this question should be answered if and when the rules are updated.

Message from the Core team

On behalf of the Core team I want to thank the community for continuing to explore
directions for improving the language support for trailing closures.

The Core team recognizes the community’s enthusiasm here and would be interested in a discussion about the broader design space for trailing closures and the requirements and tradeoffs involved in evaluating different options. As noted in the acceptance, we consider SE-0279 to be an incremental addition that does not rule out future directions. The Core team plans on coming back to the community shortly with some guidance, which will be in a form of soliciting participation on that broader discussion, before scheduling a specific review for any proposal in this space.

Thank you again to everyone who has invested so much personal time and energy into this topic. The Core team will be circling back shortly.

55 Likes

+1
This would fix everything I dislike about trailing closures, please do this.

1 Like

I too was left unsatisfied with the state of the language after SE-279 and I was frustrated with the Core Team for accepting it. I realized, though, that closure behavior wasn’t what the proposal meant to change, its purpose was to add support for multiple trailing closures. Adding even this simple feature (optional first labels) would overcomplicate the proposal, whereas proposals have to be focused at fixing one thing at a time. Nonetheless, I’m really happy that this proposal brings support for optional first labels. I have been experimenting with the master build, and lack of the first label has really been bothering me.

2 Likes

I support the pitch as written. My inner purist/pedant really wants enforced labels for consistency, but I expect it would be unpleasant in practice with many existing functions, and not only because they weren’t designed that way.

For an example, a case that has been mentioned in this thread is Dispatch.

someQueue.async { doStuff() }
someQueue.async(execute: doStuff)

To me at least, the unlabelled case is preferable for use with a closure literal, while the labelled case is preferable with a name. (execute might not be the ideal label, but that’s beside the point).

If we had had this discussion early in the evolution of the language, we might have discussed ideas like optional labels for all types, e.g. func async(execute? work: @escaping () -> Void). However, I think this level of syntactic tweaking is well below the threshold of what’s reasonable at this point.

2 Likes

I would be strongly against mandatory labels.

Yes, there are some APIs that could benefit from always having a label, but to do that at the expense of every other API seems like overkill.

1 Like

It won't be at any other API's expense, though. Just like existing non-trailing parameters, trailing parameters can omit labels with _.

2 Likes

Agreed. They should really just be removed from the language entirely.

Here's the thing about mandatory vs optional labels:

Nobody (I hope) would support callers of a function arbitrarily eliding labels like:

func frob(removing element: Foo) { ... }

frob(myFoo)

We all know we have to call it like:

frob(removing: myFoo)

The rules of Swift (outside of trailing closure syntax) are pretty clear. If an API author gives an argument an external name, callers must use it. If the API author determines that an external name doesn't provide any clarity to the call site, the author can elide the external label with _

func frobnicate(_ element: Foo) -> Frobnicated { ... }

And we call it as:

let frobbed = frobnicate(myFoo)

When we say we'd prefer requiring closure labels, we mean, "wherever specified by the API author".

If I'm writing an API, and I know that declared labels are required in either internal or trailing position, I'll design my APIs like so:

func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]

expecting users to call the function like:

let mapped = myThing.map(xform)

or

let mapped = myThing.map { $0.thing }

If I as the API author decide that my API needs a clarifying label:

func remove(firstMatching test: (Element) -> Bool)

Then It seems clear to me that the call sites should look like:

myList.remove(firstMatching: myTest)

or

myList.remove firstMatching: { $0 == true }

but certainly not:

myList.remove { $0 == true }

Having said that, as I said, I know that this would cause all manner of disruption with current APIs etc... but I still feel that the sooner we achieve a strict consistency in the rules, the sooner we get to strict consistency in API designs.

25 Likes

Ah, I was under the impression mandatory labels were more along the lines of this (labels required no matter what).


This would be the big one for me. Affected APIs would either have to update in sync with the label change release or face code breakage. Having to write DispatchQueue.main.async execute: { (even temporarily) would not be pleasant.

It’s difficult for sure. Unfortunately, the price of not being that strict is that we’ll be dealing with API inconsistency for the next decade or longer, which IMNSHO, is way worse.

2 Likes

There is a middle ground that allows declarations to opt in to strict label checking by introducing an attribute

2 Likes

Frankly, I’m highly skeptical of an attribute as a long-term solution to this. I expect this will lead to many code bases habitually using it everywhere, which will have the crufty, legacy-heavy feeling of modern Objective-C’s nonatomic on every property and NS_ASSUME_NONNULL_BEGIN in every header.

(An established non-goal of Swift is supporting language dialects through compiler flags; language dialects through ritual incantations don’t seem any better to me)

7 Likes

An attribute would allow time for a managed transition to non-optional. Release optional label + “required label” attribute this year. Announce that the optionality will go away in say one year.

New API designs will use the attribute to ensure their users don’t need to make any changes down the line.

Existing users can use the optionality period to clean up their existing code on their own time frame.

Existing APIs can choose to adopt the attribute at a timing that makes sense to them - perhaps bundle it with an update that was going to require users update code anyways. And if you’re a user consuming a major API that updates this way, that might be a good time to go through the rest of your code and add the optional labels to get ahead of the game.

A year or two down the road we have the desired state without breaking everything all at once.

I think we understand the motivation quite well. Jens points out that attributes are permanent additions to the language. Recent such additions have enabled inlinable functions, dynamic callable types and function builders. We have never used an attribute for transitioning between language versions, and I think Jens is arguing that it would be contrary to good practice to permanently expand the syntax of the language for that purpose.