SE-0279: Multiple Trailing Closures

I don't think anyone brought this up yet in this thread, but the property accessor design is not a good basis for this. I'm sure the property accessor design would be different if redesigned today. If I remember correctly, it prevented implementing the original design for property behaviors (this later evolved to become property wrappers). In fact it surprises me that this went to proposal with a design similar to property accessors.

The property accessor syntax has caused some pain in the past since it can be a getter or a set of getters/setters/observers depending on how nested the braces are. That has major implications on parsing and adding syntax to the language since there are now two things that are very ambiguous surrounded by braces. I think the core team may be explicitly trying not to design something similar to property accessors again.

With the proposed syntax, let's say we want to add a new feature to Swift in the future that allows us to label closures in a different context– well that would confuse the parser. In the counter proposal the labels are bounded by the function call so they wouldn't affect any other thing we might want to use labels for. Since closures don't have anything to uniquely identify them, you can think of it like all syntax allowed in a closure from the parsers perspective will mix in to the labeled trailing closures level and vice-versa. Certainly some of the issues can be worked around with more parser complexity, but that can affect error messages, compiler performance, and make some syntax changes impossible in the future.

You are going to run in to this problem any time you have something like a closure or function that can also be something else that are both surrounded by curly braces making it difficult for the parser to figure out the context.

EDIT: As @allevato mentions in a reply, the proposed solution or any solution inspired by property accessors will not fix the ambiguity. In fact you really don't want closures in a context where they are ambiguous even if they are not similar to property accessors. The label and lack of nesting in ambiguous braces addresses that in the counter proposal.

Could the label be inserted into each trailing closure expression?

// One trailing closure.
foo(labelA: 5) { labelB: $0.bar }

// Two trailing closures.
foo(labelA: 5) { labelB: $0.bar } { labelC: $0.baz }

I'm not sure how it would be formatted across multiple lines.

UIView.animate(withDuration: 0.7, delay: 1.0) {
animations:
  self.view.layoutIfNeeded()
} {
completion: finished in
  print("Basket doors opened!")
}

That would be ambiguous with statement labels for do, and while, and even if it wasn't it would require arbitrary lookahead to differentiate it from closure parameter labels.

1 Like

Exactly, which is why we have the gap today where you can't use a function call with a trailing closure in the condition of an if, guard, or while statement. Upon seeing the {, the parser cannot know whether what preceded it was a complete property access or function call expression, so it can't know whether to interpret it as a trailing closure or not. Allowing a label to precede the { resolves that ambiguity, and putting braces around the whole block again puts the ambiguity back.

This would be ambiguous to parse vs. labeled loops:

foo(labelA: 4) {
  // is labelB: the closure label or the label for the loop?
  labelB: repeat { ... } while foo
}
4 Likes

I'm -1 on this. The problem isn't large or meaningful enough to have such a drastic addition to the language, IMHO. I think this proposal is rushing to add sugar on something that doesn't need it.

1 Like
  • What is your evaluation of the proposal?
    I'm -1

  • Is the problem being addressed significant enough to warrant a change to Swift?
    The delta between proposed syntax and existing one is so thin that I don't see a lot of winning with the proposed one. Yet it would add yet another way of declaring closures and it would be ambiguous with function builders.
    Real issues with multiple closures is Xcode indentation.

  • Does this proposal fit well with the feel and direction of Swift?
    No I don't think so. As a "proof": example is based on a ObjC API (UIView) and not a Swift one.

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
    I read the proposal and a lot of comments from the thread.

If for any reason the syntax was accepted anyway (:broken_heart:), please make arguments labels a requirement to use the syntax.

1 Like

I see now that we're not talking about the same thing, apologies for the confusion. I thought you were addressing single-line trailing closures specifically, not single trailing closures in general, which can be multiline. I do agree that it's worth examining how the feature affects single trailing closures generally, but in doing so I don't see a problem.

Your examples all use single-line formatting, which is the case that I think looks bad but has no reason to be used over non trailing closure syntax. I do think it works well with multiline formatting, whether the closure itself is single or multiline.

someFunction(firstArg: 5) {
    closureArg: { $0.someProperty }
}

someFunction(firstArg: 5) {
    closureArg: {
        var x = $0.someProperty
        x += 2
        return x
    }
}

Sure, it doesn't look good if you try to condense it all onto one line, but lots of syntax doesn't look good on a single line. That doesn't mean there's a problem with it. Non-trailing closures work just fine if you want a label + single-line formatting.

I understand your perspective much better now, thank you for clarifying. I interpreted "smallest delta from the syntaxes we're already familiar with" to mean that the syntax should feel familiar because it can be related strongly to existing syntactic concepts, while you seem to have meant that it should have the smallest possible transformation of literal characters from existing trailing closure syntax.

While I think that's a valid aspect of syntax design to consider (extra syntax shouldn't be added without reason), I think there are many other aspects to consider as well, and I don't agree that it should be emphasized as the primary design goal.

By trying to change as few characters as possible, a new syntax has been created that is fundamentally different than anything that exists in the language today. That is why I see it as more disruptive and less familiar than the proposal.

There absolutely is a reason, which has been mentioned upthread—sealing existing grammatical holes in the language. Unfortunately I fear that I'm repeating myself, so I'll keep this as brief as possible. A style guide or linting/formatting tool may want to enforce a rule that a method that can be called with a trailing closure is always called with a trailing closure. For example, would you agree that the prevailing convention in Swift is to write this:

let squares = numbers.map { $0 * $0 }

and not this?

let squares = numbers.map({ $0 * $0 })

If so, then it makes sense to enforce that through tooling. But, because of parsing ambiguities, that rule cannot be applied uniformly. If someone writes this:

if array.contains(where: { $0.isABadThing }) {

and tooling looks at that function call, says "hey! that can be a trailing closure!" and applies the transformation, we end up with

if array.contains { $0.isABadThing } {

which no longer compiles. We either have to ignore function calls in conditional statements, or play some other trick like put parentheses around the entire expression:

if (array.contains { $0.isABadThing }) {

...but now that conflicts with a rule that says "the condition in a conditional statement should not be surrounded by parentheses." Language limitations would require these two otherwise unrelated rules to be coupled together, which is a weird special case.

Inserting a label before the closure makes the problem go away, because the brace following the label must unambiguously be a closure:

if array.contains where: { $0.isABadThing } { ...

And now we can uniformly apply the trailing closure rule above.

Requiring braces around the labeled closures brings the ambiguity back again:

if array.contains { where: { $0.isABadThing } } { ...

This is what I mean by looking at the problem holistically. When you said that there was no reason to use a particular syntax, I think that you may have only been considering what a human might write from scratch. But when you work with source-based tooling, having a syntax that removes ambiguities from the language and allows for consistent code without decreasing readability is an excellent reason to prefer it over a syntax that doesn't.

2 Likes

EDIT: This is really unintelligible, so I'm reposting a clearer version.

EDIT: I came up with an idea of using metafunctions similar to function builders to represent multiple trailing closures. It is close to the alternative syntax suggested by Chris Lattner, but instead of two types of arguments, it has one type of argument and a function call builder metafunction. Since this effectively would bring us back down to one type of argument through metaprogramming it may be appetizing to more people. I've been appending this as ideas have occurred to me– sorry this post is more of a brain dump. Does anyone else think this is Swifty?

The alternative proposal wouldn't be introducing any new syntax other than white space as a delimiter. Is that the aspect you have an issue with? It would use labels much in the same way as they are used elsewhere. I'm not sure what other Swift feature we would draw inspiration from other then property accessors, but it is a bad idea to copy that design due to it being too easy to introduce ambiguity.

I suppose we could make them look like modifiers if we want them to look like another swift feature. I could get behind the look of this, but I haven't thought this through fully.

EDIT: In fact I find this somewhat appealing because modifiers are normally used along with trailing closures. They would sort of compose visually.

EDIT 2: "." implies chaining or continuation from the previous line similar to view builder modifiers but with a slightly different syntax. It would still look good with indentation removed. "." is used to compose functions in an expression, so I think it could also be used to compose a trailing closure in an expression. I think of it like a metafunction that adds an argument to a function.

EDIT 3: I think this syntax could also be extended to general "trailing arguments". Normally if I have an argument that is big and awkward– like a list, dictionary, or verbose function call, I would set it to a constant variable that only gets used one time inside an expression. With "trailing arguments" it could just be moved inline and I wouldn't need to pollute my local namespace with single use constants. Sub-expressions would need to be in parentheses.

EDIT 4: Maybe unlabeled single trailing closures could optionally be written "array.prefix .: { $0.someProperty }"–a variation of the syntax below–to help with the ambiguity following the call builder metafunction theme.

EDIT 5: For SwiftUI this would essentially be a Function Call Builder inside a View Builder which I think is kind of cool.

UIView.animate(withDuration: 0.7, delay: 1.0)
    // Think of this as a metafunction that adds an argument
    // to the `animate` call using a function builder pattern.
    .animations: {
        self.view.layoutIfNeeded()
    } 
    .completion: { finished in
        print("Basket doors opened!")
    }

// Could remove whitespace when composing:
UIView.animate(withDuration: 0.7, delay: 1.0).animations: { self.view.layoutIfNeeded() } 
    .completion: { finished in print("Basket doors opened!") }

// Combining with modifiers
Button() 
    .action: {
        // ...
    } 
    .label: {
        //...
    }
    .padding(20)

Button.action: {
        // ...
    }.label: {
        //...
    }.padding(20)

// A call builder metafunction called `.:` to allow unambiguous unlabeled last argument
array.contains.: { $0 == 1 }

Isn't this a bit too similar to where clause in for loops?

for x in array where x.isABadThing { ...

It's two completely different syntactic construct but they look uncomfortably close to me.

Regardless of which syntax is chosen I think I'd prefer if functions calls in if conditions were still required to be parenthesized. This is a bit more inconvenient to type, but I find it provides more clarity to the reader at a location where you're expecting { to open a scope for the if.

Only because I happened to choose an example with a function that has where as its argument label, since it was the first one that came to mind that returned a Boolean value and felt clearer when the label was present than when it wasn't.

I don't think the discomfort introduced by that coincidental case should determine what we do for all functions where the closure label can be any arbitrary identifier that the API author wishes.

I don't think it's a coincidence you ended up with where: as a label. What I find is that most of the interesting functions with a trailing closure that you might want to call in an if condition either have a closure parameter label named where: or no label at all.

That's an unfortunate legacy that we've been trying to do better at. For closure parameters that are likely to be used with trailing closures, API authors should be thinking about how it would read if someone passed in a function value.

2 Likes

Perhaps if you focus primarily on Boolean-returning functions, but we needn't limit ourselves in that way—this principle applies to any function call as part of a larger expression where the trailing closure's left brace would appear un-nested in the conditional expression. These are also parsing errors, for example:

// prefix(while:)
if array.prefix { $0.someProperty }.isEmpty {
  ...
}

// max(by:)
if array.max { $0.value < $1.value } > 0 {
  ...
}

So it's not a strain to find examples where a trailing closure would be compact and readable in the standard library APIs alone—ignoring other third-party frameworks where any label could be used—but they are currently syntactically invalid.

The counterproposal doesn't fully solve this problem. To get as close as possible, you would have to start requiring as part of your linter that trailing closure labels must always be used.

let bool = array.contains { $0.someProperty } // this is now forbidden

That's okay if that's what you like, but not all trailing closures have labels.

if array.compactMap { $0.someProperty }.isEmpty {}

This is still a parsing error, and there is no way to fix it without parens because there is no label to insert.

But taking a step back, can we not make our linters and formatters smart enough to handle the language as we'd like it to be written instead of designing the language to be more easily linted?

I also really feel that tackling this ambiguity problem is not something this proposal needs to address. It's not what it set out to fix, so it's something that would be more appropriate to examine fully in its own separate proposal.

Yes, my issue is with delimiting the trailing closures purely with whitespace. Doug used the term juxtaposition earlier, which I guess is the proper term for it.

I'm not sure that a modifier-like chaining syntax works because it doesn't seem like it could be disambiguated with an actual chained method call.

someFunction(x: 1)
    .somethingElse {} // is this an argument to someFunction or a method call on the value someFunction returned?

Right, juxtaposition is better terminology– this is coming from someone who doesn't work on compilers for a living :). The syntax I proposed was different then a method call (note the colon):

someFunction(x: 1)
    // This acts like a metafunction that adds an argument to the
    // function– like a function call builder.
    .somethingElse: {  
        print("from trailing closure") 
    } 

Ah sorry! I should have looked more closely. I think that's unambiguous (not an expert though), and from an aesthetic standpoint it does harmonize with regular chaining syntax nicely. From a human-parsing standpoint I think it blends in a little too well. But it's an interesting idea, and I'm happy to see other designs being put forth. Maybe there's a design lurking out there that will make everyone happy :smile:

2 Likes

My thought was a function call builder would look like an argument. If this idea is interesting, it might be worth exploring metafunctions in other contexts to come up with syntax.

EDIT: It just occurred to me that Swift already has metaprogramming in the form of literal expressions (#file, #column, etc) and conditional compilation blocks (if, #endif), so something like this might work for a function call builder metafunction– I'm not sure if this is unambiguous:

Button() 
    #action: {
        // ...
    } 
    #label: {
        //...
    }
    .padding(20)

The main thing I don't like about this is "#" doesn't really mean composition– although it already changes meaning based on context. I'm not sure it's important to consistently use "#" with metaprogramming. The "#" is used for the other features only because those features are like preprocessor macros. It may be more accurate to say "#" is a macro prefix. I still prefer my original syntax: .action: {...}.

1 Like

Random thought, are we using the caret anywhere?

Button() 
    ^action: {
        // ...
    } 
    ^label: {
        //...
    }
    .padding(20)

To me, it kinda looks like explicitly attaching the closure to the function call above it.

4 Likes

I was thinking a caret would look good asthetically, but assuming there isn't ambiguity I would hate to burn a symbol on something like this.