SE-0279: Multiple Trailing Closures

The existing trailing closure syntax reduces the nesting level of delimiters by moving the {...} outside of the (...). The proposed syntax does not reduce the nesting level; it just changes the outer delimiters. To me, that reduction of nesting is what justifies the existing syntax. The human brain is just not that good at dealing with nesting.

(I think this is also why many people seem to find Swift method call syntax so much more palatable than Objective-C message syntax: object.this().that() is easier to understand than [[object this] that].)

10 Likes

And this is a major gain from my point of view. Having mixed delimiters in deeply nested code is a pain, and if this let me remove one case where I have to mix such delimiter, this bring as much benefit than reducing the nested level.

But this proposal doesn't eliminate (withDuration: 1) :thinking:

Yeah, I agree with this point. I don’t have an issue with Apple and the Core Team making executive decisions about the language, but communicating on the role the community should be playing on a review thread of such a feature should be clarified ahead of time. Otherwise we are at risk of the community feeling as though their feedback is being dismissed or ignored (which in some sense it is, but there’s a very big difference between being asked for feedback and having it dismissed, and being told from the outset that the scope of the desired feedback is limited).

FWIW, if this proposal is in the same boat that function builders were last year (i.e. “this proposal will eventually make it into the language in some form), this is the version that I would like to see. Given that this feature requires you to wrap each trailing closure in its own set of braces anyway, and label them with argument labels, it doesn’t really feel like there’s anything closure-y about the “trailing closure block”. It just feels like an extension of the parameter list, which has been arbitrarily broken up into two sections. I’d prefer having it be an all-or-nothing proposition for the entire argument list, not just closures.

5 Likes

Just so that I understand you correctly, would you want to have this feature 'as proposed' or the way I wrote it somewhere upthread?


More feedback on the proposal. I think maybe the name of the feature is a little bit misleading in case we really want to generalize it even further. It almost feels like a 'multi-line-trailing-parameter-list-closure'. The closest analogy here would be the multi-line string literal """ vs. the single-line string literal ". However this new syntax form can be used in combination with the standard parameter list, even though technically the standard parameter list can already be used throughout multiple lines.

The design for a 'multi-line-trailing-parameter-list-closure' is fairly straight forward.

If we had a function with the following signature:

func foo(
  _ closure_1: () -> Void, 
  _ closure_2: () -> Void, 
  flag: Bool, 
  _: Int
) { ... }

We can transform the parameter list in a closure like syntax, but it would require you to write it throughout multiple lines. The trailing parameter list closure can start at any parameter point and therefore all of these forms would be valid.

let boolean = true

// if it's a single parameter in the 
// trailing-parameter-list-closure
// then we don't need to force a new line
foo({ print(1) }, { print(2) }, flag: boolean) { 42 }

foo({ print(1) }, { print(2) }) {
  flag: boolean
  42
}

foo({ print(1) }) {
  { print(2) }
  flag: boolean
  42
}

// `foo() { ... }` would also be allowed,
// but we can omit `()`
foo {
  { 
    print(1) 
  }
  { print(2) }
  flag: boolean
  42
}

Since the order of parameters is already strict, the labels shouldn't be required in theory, but from the call side they would help readability a lot.

The following forms are illegal, as the parameters should always start on a new line.

foo {
  { print(1) } { print(2) } flag: boolean 42
}

foo {
  { print(1) } { 
    print(2) 
  } 
  flag: boolean 42
}

The above examples are intentionally made not visually appealing, to simply show what we could potentially write. However I strongly think that we'd need that much flexibility to avoid edge-cases that would work better with such declarative-ish design, but communicate the correct use through good design guidelines.

By the way the above would also apply to init, but that's trivial.

If I understand it correctly (the entire argument list would be placed in the braces, not just the trailing closure) I prefer your suggestion to the as-proposed version.

I see, thank you for the clarification.

Borrowing the with function from @Erica_Sadun's old pitch (with some hypothetical and explicit self rebind), combined with the idea I presented above to create a more appealing usage example:

let label = with {
  UILabel(frame: someFrame)
  update: { [self = $0] in
    textAlignment = .center
    font = UIFont(name: "DnealianManuscript", size: 72)
    text = questionText
    numberOfLines = 0
    mainView.addSubview(self)
  }
}

Honestly, this reads extremely good to me.


And yes I know you could also write it like so:

let label = with(
  UILabel(frame: someFrame),
  update: { [self = $0] in
    textAlignment = .center
    font = UIFont(name: "DnealianManuscript", size: 72)
    text = questionText
    numberOfLines = 0
    mainView.addSubview(self)
  }
)

And yes in this example the difference is really minor, but the general feature from my point of view would allow you to split the parameter list into a multi line trailing closure like parameter list at any point, which is by itself a great flexibility to have.

1 Like

Honest question, what is this?

parsing this visually, I'd say with is a method with a trailing closure, then, I guess this closure uses function builders cause otherwise the UILabel is an unused instance of a label. For the update: I am at a loss… is this now one of the proposed new ways of writing multiple closures? And the unanchored property names (which I am now guessing was part of the mentioned article) how on earth is one to know that those are properties of … well, where do those belong to actually?

This is incomprehensible to me. (which might be more an issue with me than with whatever is that is being proposed, sure…)

Edit: Ah, you updated your post. sry, I didn't see that.

2 Likes

Just before I can reply any further to your questions can you confirm to me that you read and understood the contents of my previous posts, here and here?


And to clarify on the content within the closure behind the update label. As I mentioned in my post it would use "some hypothetical and explicit self rebind", which allows you to omit self (similar to SE-0269) by explicitly introducing it to the closure to rebind $0, which then would allow us to omit several points of $0 usage.

The explicit syntax I used to describe the rebinding was:

[self = $0] in

Please note this is bike shedding of a different non-exiting feature right now.

Without any of the features discussed in this thread or on my posts, the original example might look the following way.

let label = with(
  UILabel(frame: someFrame),
  update: {
    $0.textAlignment = .center
    $0.font = UIFont(name: "DnealianManuscript", size: 72)
    $0.text = questionText
    $0.numberOfLines = 0
    mainView.addSubview($0)
  }
)

There are no function builders involved in here. The 'multi-line-trailing-parameter-list-closure' would essentially be transformed into the standard parameter-list syntax.


No worries. ;)

Can this really work with label-less parameters? The line "UILabel(frame: someFrame)" is already valid as the first statement of a closure. Not until the following "update: {" line would the parser be able to backtrack and figure out that this is an argument list.


I think this is also confusing with loop labels. Consider that this is valid syntax today:

with {
  UILabel(frame: someFrame)
  update: while true {
    ...
  }
}

And if we ever decide to make the if/else statement an expression, it'd make this valid syntax too:

with {
  UILabel(frame: someFrame)
  update: if true {
    ...
  }
}

Except now it's syntax for an argument list (because you can't have a loop label preceding an if).

1 Like

I read this thread up and down, and also your previous posts (and just to clarify here, I think that everyone here, you included, has interesting and creative ideas) but it illustrates my point exactly. None of the new syntax proposed in any of the posts stuck enough to my brain so I could understand your sample.

To be fair, of course the unfamiliar syntax of the "borrowed" rebinding code didn't help, but the sample of your response is a lot easier to parse than anything else, because it is familiar and unambiguous. I mean $0 is super concise and immediately transports the right information along that's necessary to know where stuff that's appended to the right of it belongs.

Not in a million years would I have guessed that the UILabel was actually the first argument of a method. And even now that I know, how on earth is this not ambiguous without end to anyone coming to the language? Couldn't this also be a function builder? Oh wait, there's the comma at the end. So no…, but doesn't someone here also get rid of those?

The whole point being that:

is imho really really not true.

1 Like

As hopefully already implied in what I sad, the getting used to is a preference, not a requirement for everyone.


I'm not a compiler developer to be able to answer this particular question. And I think the question really is: Is the following technically possible?

func bar(_: Int, value: Int, _: Int) {}

// notice there are no more commas
bar(
  1
  value: 2
  3
)

If the answer is yes, then it should be possible to write the same, but with brackets. That again is a speculation from a non-compiler developer.

The above should answer the question, but I have to add that languages differ and you cannot read and understand every different language right away without learning some of its syntactical differences.

It might be confusing, but I don't think loop labels would be allowed for expressions such as if/else, switch, etc. and therefore this syntax should be unambiguous.

My whole point of the ideas that ignited in my brain through this proposal is that a feature like 'multi-line-trailing-parameter-list-closure' would allow us to write either the full parameter list or a trailing sub-set of it, but within brackets and with clearly defined rules.

And exactly this "writing a trailing subset of the parameter list in a visually different closure-like from" is what being proposed in this thread, not just "multiple trailing closures", which is too strict and not flexible in its usage.

But I can expect that there are not vastly different representations for the same constructs that require heavy visual bookkeeping and backtracking in what you already parsed to come to a conclusion of what you see. Sure, for a parser that's fine… for a human not so much. (The example here being that I have to spot the comma after the UILabel to retroactively know that I wasn't looking at at trailing closure with a function builder but something else.)

5 Likes

For compiler, probably not, but for human, it can gets tricky with multiline expressions.

bar(
  1
  value: 2 +
    1 // huh?
  3
)

Sure, I don't disagree with you on this example. We can already have that situation in some function builder context as well though (if we'd ignore the label from the example).

// imagine some SwiftUI in here
buildView {
  Text("1")
  /* label: */ Text("2") + 
    Text("1")
  Text("3")
}

The issue with {} that you don't have with () is that the former is already in use for closures. If what you put in your {} can be parsed both as a closure and an argument list, then the syntax is ambiguous.

Consider this:

func foo<T>(_ arg: T) {}

foo {
   bar()
}

If the braces represent an argument list, then it's equivalent to foo(bar()). But this is also a valid trailing closure in today's Swift and it means foo({ bar() }). It can't be both, obviously.

For the thing to be unambiguous, it needs to have at least one argument label. Then the parser could start as usual, assuming it's a trailing closure, and then backtrack and reinterpret everything as an argument list on the first label it sees (that isn't followed by a loop keyword). That seems like a recipe for confusing diagnostic messages however: if reinterpreting as an argument list fails too, it won't be able to tell you whether your argument list is malformed or whether you have an incorrect statement in your closure.

Honestly, I'm pretty sure we can't have label-less parameters appearing in a brace-delimited parameter list.


It can made unambiguous for the parser by looking at the keyword that follows. But for the human eye I think it'll be extremely confusing if this:

foo {
   label: while true { ... }
}

is a trailing closure as in

foo({ label: while true { ... } })

while this:

foo {
   label: if true { ... } else { ... }
}

is an argument list as in

foo(label: if true { ... } else { ... })

Obviously, that's only a problem if we want to pursue the idea of making if/else an expression.

5 Likes

After almost 200 replies in less than a week, I have yet to see a compelling argument in favor of the proposal as is - or for an alternative.

I'm a strong -1 on this because it's introducing a new syntax that does not replace or objectively provide new ways for writing more correct code.

While I don't think this would be a fundamental shift in the language if it were to be accepted - such as if Swift changed to allow implicit conversions - as it's just syntax sugar.

However, I would immediately be looking for a linter to enforce not allowing use of this form, contradicting the motivating arguments in favor of this proposal. It will go down in the category of endless arguments of personal preference that we already have regarding coding styles.

Even with @DevAndArtist's proposal of expanding the syntax to allow all parameters be expressed as the proposed syntax, that makes it worse in my opinion - even if it's a bit more aesthetically pleasing or easier to read.

My concerns with this:

  1. It is not replacing existing syntax, it's adding yet another way of writing closures that individual teams / projects will determine what they like best and using a linter to enforce it.
  2. This is an objectively worse change to Swift, when comparing precedent to previous proposals and discussions on both syntax changes and additions to the Standard Library.
    • In quite literally many cases, it is just changing ( to {
    • It does not make writing correct code easier, compared to what is already possible today.
    • It does not make reading code easier, as there will be new noise and a new way of entirely writing that developers who aren't following Swift Evolution will be caught off guard by. There will be dozens of StackOverflow questions of "What is going on in this closure? I thought it was written as ..."
  3. My understanding of the original intent behind trailing closures was that it was syntax sugar for a single closure argument. Just as using the $0...$n syntax gets unwieldy after perhaps 3 arguments, this is all developer preference on when to use one syntax over the other.
21 Likes

That's some good feedback, I really appreciate that observation, thanks.