SE-0279: Multiple Trailing Closures

Do empiric measures change your favorite ice cream flavor from vanilla to chocolate? A subjective experience isn't always malleable to objective data.

If aesthetics don't matter...why not just use assembly? The whole point of a programming language is to translate what our human brain wants to do into a language a computer understands.

The point of syntactic sugar is to make certain use cases easier for certain users who want to use them that way. If you prefer a different way, that's fine! That's the point of it being sugar not "the one and true way to write this expression". AFAIK this proposal doesn't add any new capabilities that were previously impossible. However it does alleviate a pain point which some people (me) have. Preferring the existing syntax is just as subjective.

The question here is #1. "would enough people prefer the new syntax option" if so, #2. "would that cause any major problems in the language, or cause significant harm to those who do not use the new syntax".

You're trying to argue about #1. You can't. If you have an argument about #2, please give it. :slight_smile:

2 Likes

This would be no more "blessed" than the trailing closure syntax for a single closure currently is:

// valid
dismiss(animated: true, completion: { /* some work */ })

// also valid
dismiss(animated: true) {
  /* some work */
}
2 Likes

For what it’s worth, this exact API has always bothered me, even back when I was writing UIKit apps for a living, and I’ve long wished Swift handled it better. My immediate reaction was not “Oh, Button will be nicer now!”, but “Oh, animateWithDuration will be nicer now!”

I’m very much in favor of this proposal. APIs that take multiple closures have always felt so awkward that we’ve preferred to avoid them; for instance, if we’d had this feature in Swift 5, we might have included these methods in the Result type instead of the map and mapError methods we shipped instead:

extension Result {
  public func map<S, F>(
    success: (Success) -> S,
    failure: (Failure) -> F
  ) -> Result<S, F> where F: Error

  public func map<S>(
    success: (Success) -> S
  ) -> Result<S, Failure>

  public func map<F>(
    failure: (Failure) -> F
  ) -> Result<Success, F> where F: Error
}

A little syntactic sugar here could go a long way.

6 Likes

Exactly! For a call with an error and success case, I used to prefer 2 arguments with a closure for each case because it forces users (me) to handle both cases. However, (partially) because I prefer trailing closures, I've switch to use Result. This proposal would allow me to switch back to my former preference, or do something like your map and get the best of both worlds!

Looking at how many people in this thread think that this isn't an improvement over the normal call form, I don't think everyone using that proposal would be the case. Even the normal closure syntax isn't universally used, even within swift repo itself, even though it has an advantage of being shorter to write, unlike this proposal for two and three trailing arguments.

1 Like

While looking at the places in swift repo where there are closures at the end of calls, but don't use the trailing closure syntax, I noticed that a lot of them are used for BenchmarkInfo initializers

  BenchmarkInfo(
    name: "AngryPhonebook.ASCII.Small",
    runFunction: { angryPhonebook($0, ascii) },
    tags: t,
    setUpFunction: { blackHole(ascii) }),

This proposal would not change that. Even more reasons against this proposal.

Google's style guide has a rule that says "if a function call takes multiple closures, don't use trailing closure syntax", which probably explains why I'm less bothered by the status quo. We just don't use trailing closures in these situations, and the code that we write looks fine with regular function calls. But I'll grant that people can legitimately see the existence of that style rule as a rationale for a proposal like this.

For what it's worth, if someone is able to come up with a syntax that supports multiple trailing closures (or why limit ourselves, closures at any position in the argument list) that provides the same DSL-like syntax as trailing closures do today to produce natural-looking syntactic constructs, I'd be more supportive of it.

This proposal doesn't achieve that. It only changes punctuation. Rather, it adds a nearly identical way to express the same thing that differs only by punctuation, when just formatting your code a certain way would achieve the same effect.

I'm not opposed to improving multiple trailing closures in general if that's what people want. But I don't think the specific proposed syntax solves the problem in the right way, and I wouldn't want to see it accepted and thus the language stuck with it without exploring other alternatives more completely.

17 Likes

It’s been said that Swift is meant to be an opinionated language. Syntactic sugar that meets the bar for inclusion should demonstrably make it easier to write correct code (that is, more likely to be compiled into a sequence of machine instructions that more does what you expect it to do).

If we are agreed that this is the sort of proposal almost entirely aesthetic in nature, and no argument is or will be made that it makes it truly easier to write correct code, then I would reiterate the point I made earlier:

I do not think that any such aesthetic grounds are sufficient for additional syntax to be added to the language. That is to say, even if it were to make Swift the Aphrodite of programming languages even in my own subjective opinion, I would argue it should not be done. This is because we have said that there should be a very high bar for syntactic sugar.

3 Likes

Do you have any specific alternatives in mind that are not mentioned in the proposal?

1 Like

Not off the top of my head—I'd love to see something like this:

foo {
  ...
} bar {
 ...
}

but it's already been pointed out that this is ambiguous unless you force a syntactically significant newline before bar. But that itself would be unacceptable, since some people prefer to format multi-clause control constructs like this:

if {
  ...
}
else {
  ...
}

So they would never be able to match.

With that being said, I'll point out that it's not the responsibility of a reviewer to offer alternatives. I wish I did have a silver bullet here, but it's also a perfectly valid position to say in review that if it turns out that the problem can't be solved the right way due to other constraints, then we shouldn't just do something if that something has other negative consequences or doesn't hold its weight.

5 Likes

Absolutely! Since it sounds like you think something better is possible, I was just curious if you had any ideas that haven't been explored yet. If a better option is out there I'd love to see it come up during the review rather than later. It just isn't clear to me what that might be.

1, that's still subjective. 2, For me, (without being able to use it), that's true.

Aphrodite being a programming language... :smile: love it!

That said, this is a Straw Man. Nobody's saying "It's pretty, so lets add it". I prefer it for a number of reasons that I've already put forth and won't reiterate here.

Wholeheartedly second this sentiment. I recognize the issue that people are looking to solve here, and don't object to the fact that the benefits of trailing closure syntax are basically erased when your API takes multiple closures. However, I would prefer to find another solution for this issue, and if the community cannot find something else I would prefer maintaining the status quo indefinitely to adopting this proposal now.

2 Likes

I'm +1 on the proposal as it stands today. A function that takes multiple closures as parameters should be just as nice to use as a function that only takes one.

Fair enough I suppose, though I'll at least note that there is recent precedent for this type of aesthetic language change, most notably function builders and implicit single-line returns.

1 Like

That's fair, (though it's also the point of Function Builders) though the comparison I draw is with the existing Property modifiers.

var foo: Int {
    get { return _foo }
    set { _foo = newValue }
}

To align them more closely, the current required : could be removed.

My mental model for this is less "DSL" and more "collection of named functions".

1 Like

Unfortunately the colon can't be removed, because then there's an ambiguity:

foo {
  bar { ... }
  baz { ... }
}

Is this a function foo(bar:baz:) with two trailing closures, or a function foo(anyLabel:) with a single trailing closure that contains two statements, which themselves are function calls that each take a trailing closure? This ambiguity can't be resolved at syntax parsing time; it would require semantic information about the declarations.

The original motivation of trailing closures is to make constructs that use them look more like other control flow constructs (if, while, etc.). Property accessors aren't a good analogy here, because they're a declaration that contains other declarations; beyond the fact that they're curly braces with words and more curly braces in them, they're fundamentally different in purpose and semantics from closure arguments to a function call.

Just a little question. Would this new syntax be also supported for single trailing closure ?

It may be useful to get some consistent code guidelines for trailing closures and also may also be useful to for the rare cases where we want to use explicit argument label, but still want to benefit from trailing closure syntax.

-0.5. I think the motivation for this proposal is strong, and I think the proposed syntax is the best possible solution given the compatibility constraints. However, I see it as an incremental improvement which does not improve on the status quo enough to justify bifurcating the trailing closure syntax.

Yes, I think the motivating UIKit and SwiftUI examples show that designing APIs which accept multiple trailing closures can be somewhat awkward today.

While this would be nice to have at times, I don't think the benefits outweigh the downsides of having two distinct styles of trailing closure syntax. Additionally, I don't see any obvious alternatives to this proposal which could unify the two syntaxes without introducing ambiguity and breaking source compatibility.

I haven't used any languages with similar features.

I followed the original pitch, and skimmed some of the other reviews.

What is your evaluation of the proposal?

I kinda like the look of it, but I'm not too sure it fits.

Is the problem being addressed significant enough to warrant a change to Swift?
— and —
Does this proposal fit well with the feel and direction of Swift?

First of all, does it make sense to reserve this new syntax to trailing closures? The increased convenience is often significant for closures because they often span several lines, but I can't figure out a reason why non-closure arguments are not permitted (other than as an attempt to present it as an extension of the current trailing closure syntax).

Taking @cukr's example, wouldn't it be nice to be able to write this:

  BenchmarkInfo {
    name: "AngryPhonebook.ASCII.Small"
    runFunction: { angryPhonebook($0, ascii) }
    tags: t
    setUpFunction: { blackHole(ascii) }
  }

or:

  BenchmarkInfo(name: "AngryPhonebook.ASCII.Small") {
    runFunction: { angryPhonebook($0, ascii) }
    tags: t
    setUpFunction: { blackHole(ascii) }
  }

instead of this:

  BenchmarkInfo(
    name: "AngryPhonebook.ASCII.Small",
    runFunction: { angryPhonebook($0, ascii) },
    tags: t,
    setUpFunction: { blackHole(ascii) })

I think what's going to happen is people will intuitively try it by themselves and wonder why it works for some parameters and doesn't for others.

If we believe this syntax is good for closures, then it should also be good for any function argument that needs to live on its own line. Restricting it to only trailing closures makes little sense to me.


Of course, allowing comma-less calls would achieve practically the same thing and be less disruptive, at practically zero learning cost. I think this would be preferable. Example:

  BenchmarkInfo(
    name: "AngryPhonebook.ASCII.Small"
    runFunction: { angryPhonebook($0, ascii) }
    tags: t
    setUpFunction: { blackHole(ascii) }
  )

or

  BenchmarkInfo(name: "AngryPhonebook.ASCII.Small"
    runFunction: { angryPhonebook($0, ascii) }
    tags: t
    setUpFunction: { blackHole(ascii) }
  )

The only reason to go with braces would be to avoid closing parens on their own line. Maybe that's worth it, but I'm not too sure.

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 the discussion thread.

1 Like