SE-0279: Multiple Trailing Closures

-1

Debatable. Making call sites to functions with many parameters readable is challenging, and this is compounded for closure parameters (which may encourage multi-line arguments at the call site). Clearly the single-closure case was significant enough to merit special syntax, so this seems to be expanding to another (smaller) special case.

This is where this proposal falls down in my estimation, for two reasons. First, it is inconsistent with the precedent of single trailing closure syntax. Second, it is inconsistent with the established precedent for what curly braces signify in Swift.

As others have noted, the special-case syntax for a single trailing closure both reduced the nesting level of delimiters in an expression and mimics the syntax of many built-in statements. The new proposed syntax has neither of those benefits, and doesn’t actually extend the existing trailing closure syntax (it isn’t like the old syntax is an instance of the new syntax).

One of the main departures of Swift syntax from C precedent is greater consistency in how delimiters are used:

  • () is used around heterogeneous, comma-separated sequences, where both order/count of terms and labels matter (used both for parameter and argument lists)

  • [] is used around homogeneous, comma-separated sequences, where order may matter, but the number of terms is not relevant to the type. (It is also used when invoking subscripts: a minor internal inconsistency in order to follow C precedent, which at least cements [] as having to do with collections)

  • {} is used around bodies that either consist of unordered declarations, or an ordered mix of declarations and statements.

This proposal adds a new special-case meaning to {} that overlaps more (but imperfectly) with the existing role of () than it does with existing usage of {}.

(Note that function builders layer new semantics on top of a subset of the existing {} syntax, rather than being something entirely new.)

This would be the first case of {} being used to contain something other than declarations or statements, and the first case where it is used with key: value terms.

My sense is that this feature might make sense if it was either pitched as something that can apply to all argument lists aimed at making thing more pleasant when calling functions with many parameters (e.g., keeping () and focusing on eliminating commas), or if it figured out a way to make this feature actually be an extension of existing trailing closure syntax and its goals (i.e., reduce nesting depth and enable syntax like that of built-in statements).

While not closely related semantically, the chosen syntax ends up overlapping with instance creation (new) expressions in Java, Scala, and C#.

In C#, new expressions allow a mix of positional arguments inside () (which map for constructor parameters) and key = value initialization inside {} (which map to property assignments performed after construction). This style is typically used for classes that have many properties such that a constructor that supported them all would be unwieldy. There is no rule related to the types of properties (e.g., only allowing closures in one of the argument lists), as the {} part is a general-purpose feature loosely based on C99 designated initializers.

Scala (and also Java, IIRC) allow for both new expressions that use () for constructor arguments, and ones that use {} to declare an anonymous subclass at the use site (I don’t recall if mixing the two is ever allowed). In this case the {}-delimited body is just like any other class body.

The flexibility of Scala class bodies (which can contain both declarations and statements that become the body of the primary constructor for a class) means that its syntax can in principle support both single-trailing-closure cases and the moral equivalent of this proposal with the same feature.

In all of these precedents I’m aware of for mixing () and {} the feature is general-purpose and builds on existing uses for {}-delimited blocks in the language.

I’ve followed the pitch thread and did a bit of study to refresh my memory of how similar syntax in other languages works.

9 Likes

I’m not a fan of this proposal. It adds an entirely new additional syntax that users will need to learn in order to read swift code, while not being really any better than the existing syntax.

This doesn’t seem like a problem that needs solving. If a function. Takes multiple closures, it usually just makes sense to be more clear and I slide them as individual arguments.

I could see something like the if/else style alternative proposed, but for all the reasons outlined in the proposal, I don’t think it would fit in.

  • What is your evaluation of the proposal?

As a multiple trailing closure, does not seem to hold true to its name. It seems this could have been presented differently...

Based on the discussion so far, this looks like an hybrid between "SE-0257: Elide commas in multiline expressions" (potential conflict raised by @xwu) and a splitting of the arguments of function in two groups: head enclosed in parenthesis and tail enclosed in curly braces (regardless of closures). Splitting example:

Personally, I would have use empty parenthesis instead of no parenthesis at all.

But in the end still just replacing some parenthesis by curly braces, shifting code outside parenthesis... so only of value to those who do not like some sequence of parenthesis and curly braces.

The proposal fails to cover some special scenario (which have been covered in the discussion, but review are about what is written in the proposal not elsewhere):

  1. Is current trailing closure syntax affected? (must be no)
  2. Can the new syntax be use for single trailing closure? (maybe better if so)
  3. How are argument without label taken care of?
  4. How current/proposed syntax look like when the keyword in is used?
  • Is the problem being addressed significant enough to warrant a change to Swift?

The original problem is a bit unclear.

  1. There's the conditional statement like structure. Would be worthwhile, but design fail to address this cleanly (the when(then:else:) does not look proper).
  2. The SwiftUI Button case. The example suggest that the current natural flow is broken, making it worthwhile (proposal seems like a perfect match here)
  3. Style regarding functions having multiples closure at the end of the arguments list, Problem here is harder to change than supported syntax, it's the inflexible of many so called proper coding styles. When a closure is the final argument one can choose: trailing closure, inline closure, variable as a closure, real function call; and this on a call by call basis within the same file and same function.
  • Does this proposal fit well with the feel and direction of Swift?

Seems to mainly fit with SwiftUI.

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

Nope

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

Read the previous discussion thread and this one.

My initial reaction is -1. I actually think trailing closures do more harm than good. There are some APIs where they make a lot of sense (map, filter, enumeration functions, etc), but in many other cases, they're confusing. Even worse, every function that has a closure as its last argument can be used with trailing closure syntax, even if the meaning of that closure is not understood without the argument label.

I'd actually propose something else - enabling API designers to mark functions as not supporting trailing closures, perhaps like this:

struct Button<Label> where Label : View {

  @trailingClosureDisallowed
  init(action: @escaping () -> Void, @ViewBuilder label: () -> Label) { 
    //... 
  }

}

which would force consumers to write understandable code:

Button(
  action: {
    doSomething()
  },
  label: {
    Text("Button")
  })
1 Like

Yes, it supports a single trailing closure. The toggle example within the proposal solution section illustrates this.

Doug

No.

Yes, there is an example (toggle) in the proposal.

With _:, as noted in a previous reply. This should be clarified in the proposal text.

The first example in the Proposed solution section does exactly this.

Doug

I'm assuming you didn't mean to have the func keywords there, so I'll ignore them. Let's consider your last example:

foo(bar)
  success: { ... }
  failure: { ... }

Just to get this part out of the way, I have concerns here about not having the trailing closures "packaged" together in a manner that's easy to scan for by humans (indentation helps) or tools like code folding (that really prefer balanced delimiters).

More relevant, however, is that this syntax has ambiguities with postfix expressions. If I write ".wibble()" after the call above, e.g.,

foo(bar)
  success: { ... }
  failure: { ... }
.wibble()

is that equivalent to:

foo(bar,
  success: { ... }
  failure: { ... }.wibble()
)

or

foo(bar,
  success: { ... }
  failure: { ... }
).wibble()

?

With the proposed syntax, the .wibble() location makes it clear:

foo(bar) {
  success: { ... }
  failure: { ... }.wibble()
}

or

foo(bar) {
  success: { ... }
  failure: { ... }
}.wibble()

This matters a lot, for example, with SwiftUI's modifiers:

Button {
  action: {
    ...
    ...
    ...
  }

  label: {
    Text("Hello!")
  }
}
.cornerRadius(8)

and any other DSL that's using that kind of fluent interface with (multiple) trailing closures.

Thank you for bringing up this syntax. We had discussed this with some colleagues at one point and found the ambiguity, but it didn't come to mind when revising the actual proposal. I'll add this to Alternatives Considered so it doesn't get forgotten again.

Doug

4 Likes

Isn't the implication that, by using the trailing closure syntax, it must be equivalent to the latter? This syntax would not be valid if the argument following failure: was anything other than a closure literal, and { ... }.wibble() would no longer be a literal.

The parser today understands that for single trailing closures, foo(bar) { ... }.wibble() is a member access over the entire function call foo(bar) { ... } and not just over the closure expression, so why would @xwu's proposed syntax introduce an ambiguity?

1 Like

I have a feeling that we may be in danger of getting lost in counter-proposals here (109 responses so far, 15% of which are from @xwu). It’s an interesting discussion, for sure, but perhaps it’s worth keeping this thread in mind: Evolution process discussion

Just a friendly reminder that we don’t necessarily need to bikeshed a perfect syntax here. If this one isn’t good enough, it can go back to the pitch phase.

4 Likes

Strong -1.

The problem identified (the annoying syntax when a function takes multiple trailing closures) is potentially significant enough to be worth fixing, but this proposal doesn't fix it.

No. This is a syntactic sugar proposal, and we have a very high bar for those proposals. We expect sugar proposals to make very common patterns more ergonomic, eliminate common boilerplate, define away common bugs, etc.

This problem is not common, and the solution proposed doesn't reduce syntax - it just changes it. I don't see this proposal as a progression, though perhaps there are other angles on this problem that could be a better step forward.

I have no comparable experience with other languages having a similar feature.

I participated in the pitch phase, read the proposal thoroughly, and read through many of the responses on this thread.

-Chris

21 Likes

That's a good point; this is something we can resolve by extending the existing rule to the last of the trailing closures in the list, and today it won't really matter because there's no way you could ever put a member on a function type. If that were to change, e.g., because I'm able to write an extension on a function type:

extension<P, R> (P) throws -> R {
  func toResult() -> ((P) -> Result<R>) {
    return { p in Result<R>(catching: { self(p) }) }
  }
}

then it feels like I should be able to write:

foo(bar)
  success: { ... }
  failure: { ... try ... }.toResult()

but this will be parsed (incorrectly, for my purposes) as:

foo(bar,
  success: { ... }
  failure: { ... try ... }
).toResult()

Of course, the existing trailing closures would have the issue I describe. The proposal as written gives a us a way out when we go down the path of extensions to structural types. Maybe we won't care or need it.

Thinking more about this example:

foo(bar)
  success: { ... }
  failure: { ... }

It feels very like most of Swift's precedent is to have delimiters around the trailing closures. Computed properties work that way

var computed: Int {
  get { ... }
  set { ... }
}

Switch statements work that way:

switch x {
  case .a:
    // ...
  case .b:
    // ...
}

If-else has come up as a syntax we should emulate for multiple trailing closures, but if/if-else requires "extra" curly braces as well:

if x {
  print("curly braces strictly unnecessary")
}

So, even though we can resolve the ambiguity in a way that works with the language today, and we might be okay with even when extensions of structural types come along, it feels like Swift leans more toward using curly braces to group things together when they're conceptually together. All those examples above could drop the extraneous curly braces (sometimes with small grammatical tweaks), but we didn't. It seems like those reasons apply here, too.

Doug

I would personally be surprised to find that this would be valid, just as I wouldn't expect foo(bar) { ... }.toResult() to be valid (as in, interpreted with .toResult() binding to the closure and not the whole call) in a world where function types had members other than .self. AFAIK in the compiler today, the compiler requires that the trailing closure of a function call be exactly a ClosureExpr, and { ... }.something would be a MemberAccessExpr(ClosureExpr) so it would not be a valid trailing closure—it would have to be passed as a regular argument.

In the examples you've given, the braces serve specific and consistent purposes in their role as delimiters:

  • A computed property's accessors are declarations, so using braces around them is consistent with Swift's usage of braces around other groups of declarations, like type members.

  • The braces around the statements in an if/else clause aren't viewed as "extra" by those of us making the analogy, but since they surround a group of statements, they're viewed as being exactly the same as the braces that surround the statements in a trailing closure. But if the analogy that you're drawing is to the fact that some C-family languages don't require braces when the body of an if/else is a single statement, I always thought that was because omitting the braces frequently leads to bugs as code grows in complexity and statements are added and Swift wanted to avoid that common pitfall. But that issue wouldn't afflict multiple trailing closures if braces were not part of the outer syntax.

  • Likewise, in a switch statement, the braces surround a group of statements (ok, a group of case clauses each of which contains statements). But even here, Swift chose to hew closely to C's historical antecedent and uses a colon without braces surrounding the individual statement groups, when it could have added braces here as well.

So based on these examples it doesn't feel like there's a consistent brace philosophy that has been applied a priori here—they strike me more as a mix of unrelated use cases that happen to have braces as delimiters.

But even if we do treat the examples above as a consistently applied brace system, we see that they are used to surround declarations and statements. There is no situation today where curly braces surround the argument list to a function or a subset of it. So if the claim is that multiple closures need to be grouped together using delimiters, then they already can be—by the parentheses that delimit their enclosing argument list. Adding a second kind of delimiter to do essentially the same thing feels superfluous—especially when compared to the alternate proposal of removing the delimiters entirely.

5 Likes

Here is wild idea to the community and the proposal authors. I‘d appreciate any feedback.

In SwiftUI we have multiple cases where we use ViewBuilder function builder to stack some views.

VStack {
  Text("a")
  Text("b")
  Text("c")
  Text("d")
}

Some of us are getting used to this kind of DSL usage and it starts to feel natural.

So here is my question: WHAT IF we generalized the idea from this proposal not only to use it for trailing closures but for all init/func parameters which we then would stack in a similar way as shown above with VStack?

Example:

// before
transition(with: view, duration: 2.0,
  animations: {
    ...
  },
  completion: {
    ...
  }
)

// after
transition {
  with: view
  duration: 2.0
  animations: {
    ...
  }
  completion: {
    ...
  }
}
func foo(_: Int, _: String, flag: Bool, closure: (Int) -> Void) {}

// old forms
foo(42, "swift", flag: true, closure: { print($0) })

foo(42, "swift", flag: true) { print($0) }

// new possible forms
foo(42, "swift", flag: true) {
  closure: { print($0) }
}

foo(42, "swift") {
  flag: true
  closure: { print($0) }
}

foo(42) {
  "swift"
  flag: true
  closure: { print($0) }
}

foo {
  42
  "swift"
  flag: true
  closure: { print($0) }
}

This would be another DSL-ish style syntax we are already somehow familiar with.

2 Likes

This is essentially SE-0257 (which was sent back for revision) with curly braces surrounding the argument list instead of parentheses.

What would the motivation be for making any function call look like a function builder, other than "this thing can now look like that thing"? I can't think of any, other than to attempt to satisfy everyone's individual style preferences, which I hope is a non-goal from a language evolution point of view. Beyond syntax, function builders and function calls are very different, so it's important to focus on what the code does and not just what it looks like.

Function builders are sequences of expressions (with some limited control flow) that allow values to be built up using a clean DSL-like syntax. There are complex transformations that take place under the hood to make this work, and the code that you write using a function builder is significantly clearer than the equivalent code that you would write if you tried to do the equivalent thing by hand yourself. (And this all has to be taken with a grain of salt, because the feature has not been officially sent through evolution yet.)

On the other hand, in your example, a function call is still just a function call, whether it uses parentheses and commas or braces and no commas. There's no semantic change, added functionality, or benefit. This would be actively harmful—not just in terms of learning the language, but for tooling that now has to recognize two different but functionally equivalent forms of function calls.

12 Likes
  • What is your evaluation of the proposal?

I'm a bit torn, as its a rough edge that has always annoyed me in Swift. But after a night of thinking about it I think a lot of it comes down to the way Xcode treats trailing closures when multiple closures are present. What I actually want is a setting in Xcode to not automatically fill out a trailing closure when multiple closures are required, that's my biggest pain point. Using the UIView animation:completion example being thrown around, I ALWAYS have to go back and change the trailing closure for that so that its balanced with the animation closure, otherwise it just appears very ugly to me.

Does this proposal fix that rough edge? In a way, yes, but at the same time, no. As others have pointed out it doesn't really reduce the syntax needed, it simply shifts it around slightly. In the end it just adds a 3rd permutation for how to call closures in functions which increases the learning curve and gives developers more things to argue about when it comes to syntax style rather than just saying 'this is the swift way, deal with it'.

With all that said; I'm all for the sentiment behind this proposal, but I wonder if it could be solved in a different way.

  • Is the problem being addressed significant enough to warrant a change to Swift?

Yes, this is a rough edge in Swift at the moment, but I'm also torn as to if tooling improvements could smooth out this rough edge instead.

  • Does this proposal fit well with the feel and direction of Swift?

In its current state, no.

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

n/a

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

Read the proposal and above replies as well as thinking about how I actually use functions with multiple closures in my day to day work.

2 Likes

I agree with @xwu.

Proposed syntax doesn't produce new usability for programmer.
This is almost same as ordinal paren style except for slight elimination of comma between closure arguments.

To be clear my opinion,
I try to explain my intention from counter side.

If this proposal syntax is accepted,
we could consider about expanding proposed idea like below.

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

// This is proposed
UIView.animate(withDuration: 0.7, delay: 1.0, options: .curveEaseOut) {
  animations: animationFunction
  completion: completionFunction
}

// This is expanded idea
UIView.animate(withDuration: 0.7, delay: 1.0) {
  options: .curveEaseOut
  animations: animationFunction
  completion: completionFunction
}

// This is expanded idea
UIView.animate() {
  withDuration: 0.7
  delay: 1.0
  options: .curveEaseOut
  animations: animationFunction
  completion: completionFunction
}

// This is expanded idea
UIView.animate {
  withDuration: 0.7
  delay: 1.0
  options: .curveEaseOut
  animations: animationFunction
  completion: completionFunction
}

From this view, proposed syntax is not seems to be related with closure syntax thing.

It is a just new second calling paren which is using not round ( but brace {.

That‘s also what I observed above. Similarly we could debate if this syntax would be any better compared to the old form where we theoretically could say that it‘s okay to omit the comma if you write the parameters vertically. If the latter would be acceptable, then this proposal is indeed just a question of preference between () and {}, except a small difference that in the proposed form the parenthesis are always closed before the trailing closure syntax begins.

Personally I would widely adopt the latter as it stylistically looks more readable, especially because my code usually grows in hight than width due to 80 character line width limit.

That said, I‘m +1 on this proposal only if it would expand the trailing closure syntax to the whole call side, not only closure parameters as currently proposed.


Note for the review manager, the core team and the proposal authors:

Since there seems to be a lot of pushback on the proposal, the authors may wish for moving the proposal back to the pitch phase for reconsideration based on the communities feedback. I think it would be a pity just to reject the proposal here and now.

1 Like

I'm not a language expert, and I'm not going to pretend to be one, but I have no idea why this proposal is getting so much criticism (i.e. -1 votes)

In my opinion, one of the things that makes Swift beautiful is not only writing effective code, one that you know expresses not only the logic but the purposes of the invariants that the code describes, as well as it 'looking good' and being simple to read.

In this regard, this proposal is clearly a step forward regarding the use of multiple closures in Swift which, as some of us are forgetting, is a language where Functions are treated as first-order-citizens, with closures being a big part of it.

So, from my end, I want to see this added to the Language. I know my sister, who's trying to learn how to code, will find it a lot easier to understand if a call site has multiple closures.

(Weak arguments, I know. But I thought I'd add my two cents.)

3 Likes

+1
I think it increases the readability a lot. I more like the naming of the trailing closure - but hope it is not encouraging developers to add more closures.

1 Like

-1 from me, I try to use the trailing closure syntax only for functions with a single closure argument.
Unless this is serving some higher purpose that will be revealed with the next SwiftUI, this proposal just gives users one more way to write the same thing - blurring the line between what is idiomatic Swift and what is not.