SE-0279: Multiple Trailing Closures

What you write isn’t possible in Swift.

The ambiguity doesn’t arise because there’s a closing brace after doSomething. So you’d have to drop the use of naked braces after case a: but not after b:.

Then there’s the problem that even contriving that scenario isn’t possible because braces aren’t used in switch statements in that way because it’s invalid syntax in Swift to use naked braces like that.

It is true that some of the diagnostics about inserting the missing do after a label and before a brace would be harder to diagnose correctly, which is a drawback, although I guess at worst two alternative fixes could be offered in that scenario.

But, in sum, the scenario you outline above would require someone to attempt to use C-like syntax in Swift, forget the missing do statements, use the already invalid C-like syntax for the second case but not the first, then make the mistake of omitting case as well as do, but only happen to make that mistake for the case that uses the invalid C-like syntax, and then be confused by the diagnostic.

4 Likes

I think this ultimately goes to the crux of my problem with this proposal.

Earlier, I stated that one drawback of what’s proposed here is that it doesn’t truly offer multiple trailing closure syntax, in that still only one pair of trailing delimiters is allowed inside which multiple closures must be nested. What you’ve enunciated is an argument against any attempt to allow more than one pair of trailing delimiters as a design goal, because having a single end delimiter is good for readability.

But if a single end delimiter is good for readability, we already have a way to write these function calls which make use of one—only it’s ) instead of }.

So you’re really down to arguing again that swapping out ) for }—or rather, having the option to choose either one or the other, and even to change from one call site to the next—is important for readability.

Moreover, you would have to argue simultaneously that having to rely on indentation is bad from a readability standpoint for multiple single trailing } as compared to multiply nested }}, but not so bad when it is necessary to clarify the depth of nested }} as compared to using a differently shaped nesting delimiter }) that actually requires no indentation to clarify the nesting.

You would also have to justify that what’s good for some arguments in terms of the shape of braces and the lack of commas should be limited to closure expressions and not all arguments.

Thus far, almost every single other commenter I’ve tried to probe on these points has conceded that there is no limiting principle for them. That is, every proponent has either stated that they’d be for SE-0257 and allow eliding commas everywhere, speculated that we should just allow { } to surround any or all arguments at call sites, and/or supported the alternative allowing truly multiple trailing delimiters. Or they have argued against both this proposal and all such extensions of it. So my question to you is: what’s the limiting principle?

11 Likes

If there were a compromise, I would personally lean toward what works best for control flow (like proposed by Chris Lattner). Trailing closures and the ability to drop the parentheses are both designed with control flow in mind.

Just because there is a new use case in function builders and their associated actions doesn't mean they should change the original intent of trailing closures. In my little experience with function builders, I'm not sure that I like how actions are typically mixed in to a SwiftUI view anyway. The common case is a closure for the action and another for the function builder. There might be a better way to work with actions in function builder structs.

I think this use may increase significantly with reactive programming becoming popular. There are also some control-flow like structs that would benefit from this in SwiftUI outside of View builder / action pairs. I think I would only be supportive of this feature if it primarily helped control flow with closures.

1 Like

-1.

As others have pointed out, I don't think this improves things. I understand the motivation, but I'd like to see other proposals that reduce the amount of indentation / syntax needed. Honestly, just passing closures as function arguments, the way you can now, is fine for functions that take multiple function arguments.

2 Likes

Short version: the proposal should probably have a very clear statement of why {} is the right delimiter for the new kind of argument list being introduced, and how this comports with the precedent for how delimiters are used in the language to date.

I’d contend that the language has strong precedent for there being two cases of {}-delimited syntax:

  • There are “code blocks” which (according to TSPL) contain statements, and are used for function/initializer/subscript/etc. bodies and the sub-blocks of all the control-flow statements. These can de facto also contain declarations and expressions, since the statement grammar includes them, but they retain a well-defined order of evaluation for the contents.

  • There are the “bodies” of various declaration forms. Most of these (e.g., struct-body in TSPL) are documented as just containing declarations. Bodies do not have a well-defined evaluation order, and instead order of declarations is semantically insignificant.

Forming a mental model of these two cases is important when learning Swift. In particular, the seemingly magical rules for a file called Main.swift are much less magical when you can understand it as changing the global scope from one of these two cases to the other (from a declaration body to a code block).

One point of consistency between these two cases is that they (currently) have no real overlap with the grammar for argument lists in Swift. Yes, a code block can contain bare expressions (since the statement grammar admits them), but argument lists also support key: value arguments and commas as an expression separator (which don’t work in a code block). Argument lists are used for tuple expressions, function calls, subscript calls, etc. The type grammar has similar productions for tuple and function types, and in the latter case the grammar calls them “type argument lists.“ Other than subscript calls, all of the existing cases of argument lists appear inside () delimiters. The precedent is strong.

(Labeled statements may have a superficial syntactic similarity to key: value arguments, but fill a very different semantic role.)

Within the mental model above (that Swift already supports) the property/accessory case isn’t confusing at all: it is merely another case of switching between a code block (for the shorthand getter-only case) to a declaration body (for the case with explicit accessors).

TSPL describes a property body as having accessor “clauses” instead of being a proper declaration body, so one could argue that it is indeed a special case (although the specialized grammar probably has a lot to do with disambiguating the two cases). Still, the semantics of a property body are those of a declaration body: the order of the clauses is unimportant, which is quite different from the case of argument lists. Computed properties don’t establish any precedent for putting argument lists inside {}.

The root issue with the proposed syntax for me is not that it is somehow confusing to tell a code block apart from a declaration body (we do this all the time, in pretty much every language with C-influenced syntax), but rather that the proposal introduces a new third category of {}-delimited term, which contains something akin to the grammar’s current function-call-argument-list production, but with a few key changes (no commas!) and restrictions (only closure expressions allowed as arguments! labels are mandatory, even for unlabeled parameters!) so that intuition from that closely related syntax doesn’t apply.

(FWIW switch statement bodies are another apparent exception to the rules for {}-delimited syntax. The C precedent is that case ...: and default are statements (and can even appear when not directly nested under the switch), so the Swift usage can still be interpreted as falling under the code-block grammar, just with a bunch of additional restrictions.

Despite using colons, the case and default syntax has no real semantic overlap with argument lists. The order of cases matters and is up to the programmer, whereas the order of an argument list is dictated by the signature of the callee.)

8 Likes

FWIW, the discussion of that alternative syntax is in full swing here. I think it's completely reasonable to post your thoughts on it. Worst case, there's another round of review and you can re-post or link there.

Doug

Yes, that's my argument---that when you go from 1 to many you should encapsulate them in delimiters because code structure matters. The alternative syntax is taking take away the delimiters and then compensating for the lack of structure they provided with with indentation.

Doug

5 Likes

I think that's a completely fair argument. If the Core Team decides that that's a requirement of any resolution to this proposal, then my preference would be "do nothing" because the language already has delimiters around argument lists and we don't need to introduce complexity with a second set that is syntactically not very different from the first.

But based on this particular part of what you said:

when you go from 1 to many you should encapsulate them in delimiters

What is your solution for how SE-0279 addresses existing holes in the language for the single trailing closure case? Since this syntax would presumably impact those as well, it's important that we not only focus on the multiple closure case.

What do we do if the closure label provides valuable contextual information?

func animate(alongsideTransition animation: (TransitionContext) -> Void)

// Swift today
animate { ... }  // unclear what this closure is
animate(alongsideTransition: { ... })  // fine, but not a trailing closure

// SE-0279
animate { alongsideTransition: { ... } }

With SE-0279, in order to get the information provided by the label, you have to add braces. The same is true for functions that need to be disambiguated based on the label of a closure argument. SE-0279 doesn't permit that to happen unless they also add braces. So in this case, the author has not gone from 1 to many. They're still at 1, but they're being forced to add braces anyway. Those braces are not providing any additional structure; they're an unrelated band-aid required by the proposed syntax to cover the existing hole in the trailing closure syntax.

This is not the same as the computed read-only property case being written as either { get { body } } or { body }, because there's (IMO) no significant reason for someone to write the long form to begin with if they only have a getter. When someone sees the shorthand form, they know that it is always unambiguously get. With a trailing closure, however, the label could be anything and is defined by the API author.

So while I understand the argument that you're making that we're compensating for lack of code structure using indentation, I would counter that by saying that the proposed syntax imposes unnecessary structure on all uses of the syntax, including the ones where it's being used simply to get around existing problems with Swift's handling of single trailing closures.

In fact, looking over the proposal again after this discussion, I don't believe that it shows the impact that this syntax would have on single trailing closures at all. That feels like a major omission; since there are places where one would want to use a new syntax to get the label back for a single trailing closure, showing how the proposed syntax would allow that and how it would look is a very important part of the puzzle.

4 Likes

This is a response to the objection that there's something non-sensible about enclosing "many" things in braces. I agree that it'd be perfectly sensible to do so.

However, AFAICT, the issue is that lots of people don't want to have braces enclosing the "many" in this case, whether sensible or not.

Do you have an argument that would make people want enclosing braces more than not having them? If you don't, I can't see how you'd expect to change anyone's mind.

Labels are also currently only used for control flow structures, and they always have a leading keyword. Disambiguating just requires a little lookahead.

1 Like

Alright, I had thought it best to limit in-depth discussion of counterproposals in a review thread, but I'll go ahead :+1:

My main objection to removing the braces is that it's incongruous with the language. There are almost no undelimited contexts in Swift, the only one I know of being switch cases, a feature with a great deal of unique, special-case syntax and behavior.

I've seen a few people say that removing the braces feels more natural or fits into the language better, but I can't see how that's the case as there is almost no precedent for it. I would be interested to see this argument elaborated -- what precisely about the counterproposed syntax makes it a natural fit for Swift? What existing syntax can it be compared to? As Doug pointed out, the original proposal is very similar in nature to computed property syntax, so there is already a well-established precedent for it.

From a more subjective, feel-based perspective, I think it looks loose and unorganized. Many of the code samples being posted have been genuinely hard for me to parse, even when it's just an isolated example of a single function call. In a real situation, nested in several layers of delimited blocks, potentially followed by chained method calls, it looks truly out of place and is a chore to read. Without the context of this proposal, if I came across this feature in the wild, I'm certain I would be quite puzzled by it, particularly so if I was new to the language or programming in general.

I mentioned this earlier, but the syntax feels like it wants to be part of a different language that scopes by indentation with significant whitespace. If Swift didn't require braces for function bodies, if statements, loops, etc, then it would fit in just fine, but that's not the case.

That is all assuming that the code has been formatted precisely as intended. Without disciplined indentation and formatting, readability quickly goes out the window, and this syntax seems particularly susceptible to mistakes and inconsistencies. I anticipate a lot of programmers having trouble with indenting the closures properly, if there's even any consensus on what properly means.

Doug's example of chaining SwiftUI modifiers from a button with this syntax is illustrative of the nuance required to make this syntax work. In that case it's best to double-indent to separate the closures from the modifiers, but in other cases you want to single-indent. If you aren't actually going to add any modifiers, it would look weird to double-indent, so we go back to single-indenting. But if we later come back and add some, now we need to switch back to double-indentation. In my experience, this level of formatting nuance is not well-received and is rarely followed consistently among a team. Most people want simple, consistent rules that are easy to remember and lint for.

IDEs can help with formatting, but only so much. This seems like a particularly hard tooling problem. Consider this case:

Button()
    action: {
        ...
    }// ← insertion point is here

If I press return, will my IDE know to stay one level indented? Probably not. But if it did:

Button()
    action: {
        ...
    }
    label: {
        ...
    }// ← insertion point is here

Will it differentiate this case and not stay indented because it knows there are no more closure arguments? Probably not. It also wouldn't be able to anticipate whether I want to later chain some modifiers and therefore should double-indent, I would have to go back and do that manually. None of this is a problem if the scope is delimited with braces.

To summarize my stance, I think that the original syntax would be a pleasant addition to the language, and the alternative syntax would be actively harmful. I would prefer rejecting the proposal over choosing the alternative syntax.

Much of the discussion in this thread has focused specifically on the closing delimiter, but the proposal does not simply swap out the closing delimiter. Being able to close out the parameter list and open up a new context for the closures is an important factor in the improvement to readability that this proposal brings, and it's what makes this feature qualify as trailing closure syntax. When I see this:

someFunction(param1: x, param2: y,
    closure1: {
        ...
    },
    closure2: {
        ...
    }
)

I'm thrown off by the first line ending in a comma and trying to match up the closing ) delimiter with the opening delimiter that's in the middle of its line. In contrast, when I see this:

someFunction(param1: x, param2: y) {
    closure1: {
        ...
    }
    closure2: {
        ...
    }
}

My eyes can immediately match the opening and closing braces -- everything is structured in a clean and organized way. It's a small difference in terms of the literal transformation of characters, but it's a significant difference in readability from my perspective.

It is telling that although the first version is valid today, I've never seen anyone format code that way.

I think you're trying to solve a different problem than the proposal sets out to solve. There is no regression in the labeling of single trailing closures, and I don't think it's necessary for this particular proposal to solve that problem.

5 Likes

I format code in that way. I suspect I'm not the only one.

4 Likes

This is a very good point. If any proposed syntax were to regress the experience of using common tools, then that is a major drawback.

However, I am confident that should any syntax be added, users and tools will coalesce around any natural way of formatting that exists. That "double" or "single" indenting doesn't work is a sign that neither is the natural way of formatting the alternative syntax under discussion. The question, though, is whether there exists any natural way of formatting this around which users and tools could coalesce.

I believe the answer to be yes; I think there are at least two, and one or the other (or something I haven't thought of) could prove to be more suitable in any particular scenario:

One natural way of formatting is similar to that of the current single trailing closure syntax; either on the same line or on different lines:

Button() action: { ... } label: { ... }

// or

Button() action: {
  ...
} label: {
  ...
}

The latter may seem strange when declaring a Button but is particularly apt in, say, the case of functions that resemble control flow statements (as we find in the proposal text):

when (true) then: {
  ...
} else: {
  ...
}

Another natural way of formatting would be not to indent at all:

Button()
action: {
  ...
}
label: {
  ...
}
  .padding(...)

// or

when (true)
then: {
  ...
}
else: {
  ...
}

Both of these ways of formatting would work with existing tools, and both visually avoid creating confusion through misleading indentation. For these reasons I would call them 'natural' (in the sense that we're not fighting against existing conventions or tooling). Experience has shown that there is no agreeing on aesthetic taste to be had in this forum, so I will not attempt any argumentation on that front.

I believe experience would cause users to coalesce around one or the other of these, or some other sensible choice that we haven't yet thought of. My point is that such choices exist. I do agree that if no such choices existed, and any formatting options we could imagine would fight against any possible tooling, then indeed we'd have at our hands a major disadvantage.

3 Likes

I'm not sure I agree that the extra indentation is necessary for chained modifiers. As has been noted, trailing closure syntax is only valid for closure literals. Applying operators to the closure itself would make the argument expression no longer a closure literal, thus precluding the use of trailing closure syntax. I don't think anybody confuses chained operators as applying to a trailing closure rather than the result with the existing syntax and I don't see why that would necessarily be the case with Chris's proposed syntax either.

Let's consider another formatting of your above example:

Button
    action: { doSomething() }
    label: {
        Text(text)
        SomeView
            action: { doSomething() }
            label: { Text(text) }
     }
    .buttonStyle(BorderlessButtonStyle())
    .padding(.bottom, 2)

I can see the argument that with no knowledge of Swift's syntax a reader might think the modifiers apply to the button's label. But as soon as you learn to read the syntax it's perfectly clear that action, label, buttonStyle and padding all apply to the button. With that context, I think the indentation here is very apt. Things that configure or modify the button receive the same level of indentation. IMO this expresses the conceptual structure more clearly than it would with the outer braces and de-dented fluent operators.

I don't think the double indentation is necessary. Does the formatting I suggest above look difficult to read?

Right, it's about both commas and parentheses. On this point, I would address to you the same question I addressed to @Douglas_Gregor. Why stop here at what's proposed in SE-0279--what is the limiting principle? Why not allow any commas to be elided and not just those between closure expressions, as proposed in SE-0257--which is not rejected--and encourage people to line break after the opening parenthesis? That would satisfy your concerns above. Why not further encourage users to adopt this kind of formatting by allowing any function call parentheses to be replaced by braces, as was suggested earlier? Why stop at only closures, and only the ones at the end of the parameter list, and not the ones in the middle, as pointed out by still others earlier? I think if there's no limiting principle, then you're really in support of some design that's really dramatically quite different from the one proposed here.

3 Likes

It's worth noting that this is a very common convention for languages that utilize fluent interfaces.

1 Like

I would love to format my code that way, but I usually don't, because I like to be able to press ⌘A⌃I to re-indent my whole file in Xcode. And if you ask Xcode to re-indent that code, you get this mess:

someFunction(param1: x, param2: y,
             closure1: {
                ...
},
             closure2: {
                ...
}
)

There are two problems here:

  1. Xcode aligns the parameters on continuation lines (closure1 and closure2 in this example) in Lisp style, aligned with the first parameter of the initial line (param1 in this example). This often leads to way too much indentation of the continuation lines.

  2. Xcode aligns the closing } of each closure argument with the first character of the initial line, instead of with the first character of the argument label.

I've been adopting this pattern to work around Xcode's problems:

someFunction(
    param1: x, param2: y,
    closure1: ({
        ...
    }),
    closure2: ({
        ...
    })
)

I'm forced to always put the initial arguments on a continuation line (even when there's abundant room for them on the initial line) to prevent excessive indentation, and I'm forced to put extra parens around the closures to to prevent Xcode from misaligning the closing braces.

If Xcode's formatter did a better job, I would format my code exactly as in your example. I don't think we need to introduce braces as a way to group trailing closures. I think we just need Xcode to format things better.

13 Likes

No, I never claimed that there would be a regression in the existing single closure syntax. My concern was that this proposal, by introducing a new syntax for multiple trailing closures, also introduces a new second syntax for single trailing closures—or at least, I presume it does by generalization, since the proposal doesn't explicitly state that this is allowed or prohibited.

Therefore, because this proposal introduces a new syntax for single trailing closures, and because other members of the Swift team and the Swift community have pointed out that the lack of a label on the trailing closure argument is a problem in the language, and because even the proposal text uses this as a motivation ("Second, that a trailing closure argument does not provide an argument label, [...]"), it is entirely within the scope of this review to consider how those cases would look using the proposed syntax and to question whether they hold their weight syntactically.

After all, if this proposal is accepted, we're not looking at adding a second syntax for calling a function with a final closure argument; we're looking at adding a third:

someFunction(firstArg: 5, closureArg: { $0.someProperty })
someFunction(firstArg: 5) { $0.someProperty }
someFunction(firstArg: 5) { closureArg: { $0.someProperty } }

If we absolutely must introduce a third function call form to improve the way multiple trailing closures can be used, then we should do it in a way that is minimally disruptive and has the smallest delta from the syntaxes that we're already familiar with—both for multiple and single trailing closures. This proposal doesn't quite get us there.

2 Likes

-1 , in addition to providing no capabilities and adding a new (verbose) way to specify things, it also adds brand new syntactic meaning for brackets.

To me, the existing syntax allowing one closure has felt closer to a curry, so I would expect multiple closures to be surrounded by parens, not braces. The braces create both a parser and mental ambiguity on whether this is a closure or this sort of impromptu 'delegate context'.

5 Likes

The syntax does an equally well job at being indentation-agnostic as the proposed syntax. The indentation is purely for us peasant-humans.

The proposed syntax feels natural because you already instinctively indent everything inside and so is not very ambiguous in indentation. Though the same does apply to the counterproposal should we have a consistent rule, as we have for the multi-line chaining.

To me, opening argument block twice is what's unprecedented, and the unbraced version is much more inline with the trailing closures by putting closure (that would easily consume a lot of space) outside the parentheses.

1 Like