SE-0279: Multiple Trailing Closures

Good lord, they really are.

That’s the thing about using SwiftUI as a motivating example: SwiftUI isn’t Swift. In fact, it’s probably the least Swift-like framework around. It makes heavy use of many private language features, basic things like value and reference semantics, or even just initialising a property, are entirely different. Essentially all of your knowledge of Swift becomes entirely useless, and all of it is just “well I guess that’s the arbitrary magic I need to write to achieve that”.

So in principle I’m deeply opposed to making Swift more like SwiftUI. That framework shows absolutely zero respect for what Swift is, or how developers understand Swift code. It can’t just invent its own syntax, then ask us to change the language to suit it.

17 Likes

While I personally kind of like the proposal, this comment caused me to think that it is likely this entire proposal and debate wouldn't even be happening if we just made commas between arguments optional or had some kind of rule where a newline in an argument list was equivalent to a comma.

Borrowed/modified from an example up thread:

UIView.animate(withDuration: 0.7, delay: 1.0, options: .curveEaseOut
  animations: {
    // do stuff here
  }
  completion: {
    // do other stuff here
  }
)

Ta-da! :stuck_out_tongue:

I think I’ve not had time to formally comment yet, so I’ll do so now. I definitely identify with the sentiment that adding another way to represent an existing thing should require extremely strong motivation because one of my least favorite things about reading Ruby code as someone who does not primarily work in the language is the fact that there are so many totally different ways to write the same things.

It’s hard to see that from the perspective of familiarity that I have with Swift, but it really does make it harder to learn a language and it has really demotivated me from learning Ruby.

I don’t think this particular new syntax is worth it.

2 Likes
  • What is your evaluation of the proposal?

Strong -1.

I don't see much benefit over the current style wrapping method parameters. Instead it shifts them elsewhere without improving readability or reducing the number of indented blocks.

I think Karl worded it very well.

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

Maybe if this could be resolved in a way that resembled control flow, without increasing ambiguity, then I think it would be beneficial. In the current form, no.

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

No, and it makes me nervous about the future of Swift. With previous work on projects in Perl and Ruby additions of more syntactic sugar should be weighed very carefully. Some syntax formats can be quite useful for specifying APIs that provide domain specific language. I've also seen code with good intentions become horribly mangled and become a maintenance nightmare.

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

Some of Ruby's syntax would be the closest I can think of, but that goes down a very different path that allows elision of parentheses and commas.

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

I've read through a good number of the pitch and review comments. I thought commenting on the pitch but decided not to thinking there was enough of an argument against the current form at that time.

2 Likes

The "error" was actually a copy-paste error when I accidentally grabbed the closing brace in the function in which I'd written the sample to bring it here. Besides, if I'd done that in an IDE that understood the syntax, it would have been seen immediately. ;)

The argument in favor of this change isn't readability, it's writability.

When you are reading code in the current syntax, with intermixed ) and }, there's no real need to distinguish between the two glyphs in order to understand what's going on. It's all just nesting and delimiting. Having } instead of ) doesn't matter much.

However, when you are writing such code, if you omit a ) or }, or interchange them by accident, then the structure of the expression you're writing is so broken that it's unlikely the compiler can point you in the direction of a fix. In addition, you almost certainly have multiple structural errors simultaneously. (Yes, I know my description is a bit over-simplified, but I'm making a point.)

The advantage of using } throughout is that it reduces the structural error possibilities to just an excess of } or a deficit of }, and you can fix the problem by basically adding or removing } at the end. (Yes, I know my description is a bit over-simplified, but I'm making a point.)

OTOH, I completely agree with @xwu that this proposal isn't actually trailing closures, and with @Karl that the virtue of the proposed syntax is with SwiftUI or other function-builder-based schemes, where there are multiple closure-like things because it's about data structure.

This is probably the fork-in-the-road moment, where function-builders shouldn't be squished into Swift executable code syntax, and executable code syntax shouldn't be deformed by the needs of DSLs. It seems bizarre to think that the same syntax will serve both goals as Swift moves forward.

There's deeper thought needed here, and I think the community should think more deeply before making Swift syntax more inscrutable.

2 Likes
  • What is your evaluation of the proposal?

+1

I am a fan of the proposed syntax. I see complaints that it is yet another thing for beginners to learn but disagree. Most beginners I predict will be more interested in point-of-use examples (e.g. a button in SwiftUI) and not be too worried about the underlying mechanism being employed to accomplish it.

Point-of-use labels are and the extra braces emphasize make it clear that a closures is being passed. The clean, unambiguous format makes it easy for style guides and automated formatting tools.

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

Yes. Multiple closures have always been a pain point -- a punctuation obstacle course for beginners and extra cognitive noise for experts. The proposed syntax is a nice fix to the problem.

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

Completely subjective but, yes, I do.

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

I haven't but ... trailing closures in Swift were one of those features that rocked my world because they allowed me, a mere user, to create APIs that looked like a language construct. How cool! This proposal weakens that a little bit in that it creates a non-C new syntax. I think this works great for a DSL like SwiftUI and other similar DSLs. Perhaps there is a concern that accepting this closes the door on solutions that map to more native looking syntaxes. In this regard, I admit that I might be missing something that I can't yet imagine.

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

Read through the proposal and some of the review comments. Slept on it and still felt +1.

I agree. Indeed, I've heard the second rule above described as a guideline from at least a few different sources, because trailing closures (prior to this proposal) get confusing when there are multiple parameters of function type. As we noted in the proposal, it's part of the style guideline described in Swift static code analysis: Trailing closure syntax should not be used when multiple parameters are of function type. With a little more searching, I found a similar request for SwiftLint (trailing_closure rule with NSAnimationContext runAnimationGroup(_:completionHandler:) · Issue #1754 · realm/SwiftLint · GitHub).

I think the proposal understates a very real problem it solves: the existing trailing closures work very poorly with multiple trailing closures.

Per this comment:

The Swift core team and the folks who work on the compiler do not subscribe to the view above. The introduction of a warning is explicitly not considered a source-breaking change, and this has been a longstanding policy. Warnings-as-errors is there for people who want to clear out every issue the compiler reports, and are taking it on themselves to clean up their code with every new minor compiler release. @anandabits pointed this out in SE-0279: Multiple Trailing Closures - #129 by anandabits.

Sure, there's new warnings about implicit tupling and untupling for patterns in Swift 5.2. Swift 4.0 did it with the @objc inference change. I'm sure I can find others pretty easily.

Doug

4 Likes

That said (and I agree with @Douglas_Gregor's points), this isn't really the kind of thing we'd generate warnings for. There are plenty of other places where there's a syntactic improvement on offer but you can still do it the unsugared way (.some(foo) vs foo? for example) and we don't warn on those despite it being clear to most people that the sugared option is preferable.

If this goes through, I'd expect the community to gravitate towards it as preferred style when applicable, and maybe use linters to encourage/enforce it in their projects, but just like people don't have to use trailing closures today, they wouldn't have to use this.

1 Like

If we're talking about ill-formed code, where diagnostics are important, the user fairly likely to have typed the wrong delimiter () vs }), and it doesn't help recovery. Good recovery here would likely take indentation into account, because that often intentionally reinforces structure.

Doug

I start to feel that this has little to do with being closures, and more to do with being long-winded in general. Especially when closures are, in the end, just variables/expression. So I think, it'd make sense to have {} be a comma-elided section for any arguments.

UIView.animate(withDuration: 0.7) {
  delay: offset
    + delay1
    + delay2
    + delay3
  options: .curveEaseOut
  animations: {
    // do stuff here
  }
  completion: completionHandler
}

The functions in my libraries is sometimes very long, from mere parameters with no closure involve. So this problem isn't unique to, albeit is exaggerated by, closures.

Also, I believe a large portion of codebase would also have something like this,

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

which does pop up regardless of the coding styles.

I was also thinking of errors popping out in Xcode while you're typing. It's especially distracting when you open a brace { and this causes a declaration below to error out, which in turn makes an error appear on the line just above because it was referencing that declaration below.

class X {
   func carrot() {
      let juice = blend() // error: Use of local variable 'blend' before its declaration
      juice.ingredients.map { // insertion point is here, I'm typing...
   }
   func blend() {
      // ...
   }
}

That's just an annoyance, and not really a new problem, but the more braces you have the more annoying it is. And I'm aware nowadays that Xcode inserts the closing brace } automatically when you type the opening one {, but sometime I copy paste incomplete snippets too.

Another thing I'd like to mention is that this may close off argument labelling on function builder since it may be ambiguous/confusing. All the more so with autoclosure expressions.

While of course future/unknown features shouldn't get too much in a way of the proposing one, we should at least keep this in mind.

Are we going too far?

Every argument you make above applies to trailing closures as they exist in the language today. Trailing closures absolutely make parameter lists (usually delimited by ()) similar to blocks (usually delimited by {}). It increases the surface area of the language, and in adds conceptual complexity to the language.

Except that we have trailing closures, and developers use them every day, and the vast majority seem to like them. Perhaps you disagree that trailing closures are a good thing; if so, I don't expect you'll want them extended at all. If you think that trailing closures are a good thing, we've already committed to all of the negatives you list above.

Trailing closure syntax breaks down when the function you're calling has multiple parameters of function type. It leads to bugs (trailing_closure rule with NSAnimationContext runAnimationGroup(_:completionHandler:) · Issue #1754 · realm/SwiftLint · GitHub) that beget coding standards (Swift Style Guide, Swift static code analysis: Trailing closure syntax should not be used when multiple parameters are of function type) to avoid them. It's a problem worth fixing.

It's also not a new problem, either. Back when we first implemented trailing closures,
there was some angst about the fact that we didn't have a way to put argument labels into the trailing closures without losing the nice syntax. After Swift 1.0, people noticed, but for whatever reason nobody got around to fixing it.

We left an expressivity gap that affects the clarity of code, and makes a popular feature unusable in common scenarios. We should fix it, not let it fester.

Doug

6 Likes

I'm not sure exactly what you mean by this. When we were looking at the design of this feature, we considered approaches that made this more of a function-builders feature, allowing one to declare argument labels in buildBlock and have them used in the corresponding closures. Is that the kind of thing you're thinking of? I can relate how the design went, but it didn't work out well because (1) you ended up having to write a lot of different function builder types, each with different buildBlock argument labels, (2) it didn't work for mixing function builders and normal closures, as with the Button example in the proposal, and (3) it didn't solve the longstanding problems of the trailing closures not working with multiple function parameters.

  • Doug

Yes, that's the feature I'm talking about, but no, I'm not proposing that we use it for this problem. I'm just pointing out that it could easily be ambiguous should we have both of them, and labelled function builder can also be independently useful.

@Douglas_Gregor do you have any comment on potential deeper generalization of the proposed idea, IF this proposal goes through?

I‘m just curious to know what you think of it. As I said in my linked post, any feedback is welcome. :slight_smile:

That's really pulling arguments out of thin air. The first one is a deficiency in a linter library that can't correctly parse (or understandably has a hard time parsing) and the second one is just someone's API guidelines about visual style…

This proposal is pushed by Apple because they decided they need this to have a new feature look nice in summer, and it is always going to be accepted. If such a thing would be community-proposed it would never pass the high standards on changing/augmenting fundamental syntax.

Sry for the harsh words but this has happened before and being silent is being complicit.
We should really just abandon this charade of this being a "proposal" and flat out say what it really is - a done deal just like function builders, where the community may add their opinion.

Again, sorry if this seems counterproductive, but this is frustrating.
This process is alienating contributors.

13 Likes