SE-0279: Multiple Trailing Closures

I agree with Doug here. This is a weird hole in the language that really should be filled if we can. That said, I think this specific proposal doesn't solve the problem. Its net result is to trade punctuation and eliminate one comma, which is not a win in my opinion.

I think that Rob captured this really well:

I was trying to avoid getting into counterproposals in this thread, because that is not how swift-evolution proposal reviews are supposed to work. However, given this thread is already huge...

I'd start with the goals:

  1. We need to support the multiple trailing closure case.
  2. We need to make it consistent with the single trailing closure case.
  3. To Rob's point above, it should reduce nesting, which is the main payoff of the existing trailing closure syntax.
  4. While we're at it, we should look at the other weird behavior of trailing closure syntax, the fact that it eats the keyword argument for the closure.

I'd also observe that trailing closure syntax is a specifically targeted way of popping one argument out of a call. It is specific to closures, but there is no reason to think that SwiftUI-like DSLs would want this only for closures: there are lots of nesting structures that could be better to flatten.

Given this pile of goals, I'd recommend pursuing a more generic approach of allowing arbitrary arguments to be punched out from calls. I'll describe a general version of this, but it might make sense to add a provision (e.g. an op-in attribute) to allow this behavior to be controlled/limited on the funcdecl side. If we were going to make this general, we would be making things like this be synonyms (listed in order of increasing craziness and impact):

   foo(bar: 1,   baz: Button())
   foo(bar: 1)   baz: Button()
   foo() bar: 1, baz: Button()
   foo   bar: 1, baz: Button()
   foo   bar: 1  baz: Button()

This would definitely be ambiguous in recursive cases, so it might make sense to limit the subexpressions allows to specific expression productions like literals, parans, braces, etc that are bounded. We can use a whitespace role or something else to support this, as we do in many other places.

With this approach, the example from the proposal would go from being this:

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

to being this:

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

Which could be alternatively formatted as:

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

Note that it eliminated the outer level of curlies, which is the main win of the existing trailing closure syntax we have.

If we had this, then we could also apply this to the existing trailing closure syntax, allowing the use of the keyword argument to disambiguate cases like this:

func foo(fp1: () -> () ) {}
func foo(fp2: () -> () ) {}

foo(fp1: {})       // ok
foo {}             // ambiguous
foo fp1: {}        // ok

Overall, to reiterate my review above, I am -1 on this proposal, but +1 on the goal of improving this aspect of the language. I request that we do so with an eye towards generality and making the language more consistent, rather than introducing a weird special case just to eliminate a single comma.

-Chris

29 Likes