Support use of an optional label for the first trailing closure

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.

54 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.

I’m not sure why it would have to be permanent??? Would we not just deprecate and then drop the attribute? Is there some technical reason why attributes are permanent in a way that other structures are not?

Also I’d say there’s a difference between introducing the attribute in the hopes that we will eventually move to an non-optional world, and explicitly introducing the attribute and optionality as a bridge to requirement. The way the proposal is worded reads to me as sort of a Trojan horse strategy. Where optionality is first, the attribute is “maybe” second and then we see where we are.

That might be the only thing that gets approved by the core team, but I’d argue that making it explicitly transitional from the get go is a better strategy for the ecosystem.

I'm +1 on this — it seems like it clears up a lot of ambiguity. However, while I'm sure that I can and will get used to these labels (and may grudgingly admit that they make more sense to be required when desired by API authors), I'm definitely against making them required immediately.

Hasn’t been mentioned above, but enforcing labels on -auto- trailing closures would make SwiftUI a right mess. (not sure why I wrote "auto")

I do not think anyone has suggested that all trailing closures, everywhere, should require labels. This pitch is proposing that the use of a label should be allowed but optional (at the discretion of the caller). Some are arguing the decision should instead be up to the callee, similar to how function declarations currently specify whether or not non-trailing-closure arguments are labeled.

(By the way, "autoclosures" != "trailing closures").

3 Likes

Labels are already mandatory for @autoclosure parameters, iff a label was explicitly specified. There is no change being made or proposed for that.

func foo(a: Int)
func bar(a: @autoclosure () -> Int)

foo(a: 42)
bar(a: 42) 

Also one more note. Your claim is completely false as the entire SwiftUI API does not contain a single @autoclosure (I just checked the *.swiftinterface file). Maybe you meant to say something else?

1 Like

Food for thought outside the scope of this excellent proposal:

I've found little or no discussion of trailing-closure syntax in the context of memberwise initialization of structs with closure properties. By way of example, this sort of syntax regularly arises in the context of SwiftUI:

struct InsetView<Content>: View where Content: View {
    let leadingInset: CGFloat
    let trailingInset: CGFloat
    var content: () -> Content

    var body: some View {
        ...
    }
}

The synthesized memberwise initializer for this struct uses each property name as the parameter name. At the call site, we have:

InsetView(leadingInset: 10, trailingInset: 10) { 
    ...
}

If labels become required on the first trailing closure, initializing the struct would look like this:

InsetView(leadingInset: 10, trailingInset: 10) content: { 
    ...
}

The label feels out of place. Why?

Answer

Because the type was designed with an unlabelled trailing closure in mind.

Of course, in this hypothetical world of required label names, the solution to a bothersome label likely would be to expressly declare an initializer for the struct with an explicitly omitted argument label for the closure parameter.

init(leadingInset: CGFloat, trailingInset: CGFloat, _ content: @escaping () -> Content) {
    self.leadingInset = leadingInset
    self.trailingInset = trailingInset
    self.content = content
}

Unfortunately, that solution would come at the expense of losing the efficiency and minimalist look and feel attendant to synthesized memberwise initializers.

On the subject of a more granular approach to memberwise initializers, see Explicit Memberwise Initializers.

2 Likes