SE-0279: Multiple Trailing Closures

I'm not suggesting getting rid of them. I'm suggesting keeping them as short as possible - ideally one line.

  • What is your evaluation of the proposal?

-1 Adding Yet Another Calling Syntax isn't worth it to me.

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

I agree that some of the patterns established in SwiftUI are a bit awkward right now. I do not believe this proposal is the right way to address them, especially when the awkwardness is currently limited to a single framework, but this proposal is about changes everywhere.

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

No.

  • 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?

I've been trying to follow along with the firestorm threads going on.

3 Likes

I hear you. Looking at my code, almost all of my actions are 1 line delegating elsewhere. I imaging there are exceptions though. There are still iteration and flow-control style views that would use more then one view builder closure. There will inevitably be more complex types like collection views in the future that will also benefit. I agree that SwiftUI patterns could be better and I think they will be.

Are there existing UI frameworks/DSLs that expose something akin to the proposed syntax?

In HTML there is a distinction between elements and attributes, which seems to mirror the way trailing closures are used today for SwiftUI. HTML attributes correspond to the “configuration” arguments passed inside () while child elements in HTML correspond to the body/content written inside {} with function builder syntax.

In HTML callback actions are exclusively written as attributes rather than elements, and elements can only have one “body” of children. In cases where multiple bodies might have been needed (e.g., an <html> element needs both a header and a body), specific sub-elements (e.g., <header> and <body>) are defined.

As a result of the above choices, HTML (and by extension, a hypothetical HTML DSL in Swift) has no need for “multiple trailing closures.”

An interesting comparison point for a declarative UI DSL would be the use of XAML for confiding WPF UIs in the .net world.

One of the key differences in XAML as compared to HTML is that there is a general-purpose syntax for transforming an attribute to use element syntax. So something like this:

<Button action=“whatever”>
    <... content ...>
</Button>

Can instead be written as:

<Button>
    <Button.action>whatever</Button.action>
    <... content ...>
</Button>

The XAML syntax thus allows a flexible choice of what arguments to pass as part of the “configuration” (attributes) vs. the “body/content” (elements).

If the proposed feature is primarily about the DSL/SwiftUI use case, if might be good to position it clearly in relation to these other examples of UI DSLs (which seem to solve similar problems in somewhat different ways).

2 Likes

I don't think comparing to XML based declarative languages or really any other language makes sense. They are bound by the limitations of XML. Certainly the proposal as written would be similar to what would be done with XML, but it would change Swift to be like XML instead of changing the DSL to be like Swift.

EDIT: It is a really good point that XAML provides two syntaxes for the same reasons it is desired for SwiftUI. Ultimately though, I don't think Swift is close enough to other languages to make comparisons very meaningful except by coincidence.

EDIT: I think it is worth bringing this particular example up when this proposal is inevitably redone.

Can somebody point to other declarative UI frameworks that face this same problem and how they solve it?

I pointed to XML-based DSLs because they are the only examples I know much about. HTML is, de facto, the most influential declarative UI DSL out there. XAML very clearly saw the same basic problem and devised a general-purpose syntax for moving “configuration” arguments (not limited to closures/actions) into the “body” area.

When I briefly looked at declarative DSL stuff in Scala and Kotlin I only found examples that closely mirror the precedent of HTML.

If there are better examples of declarative DSLs that would be more relevant for comparison then I’d like to hear about them. As it stands I don’t think it is unproductive to compare to the only prior art I’m aware of, when the proposal doesn’t list any more appropriate prior art.

4 Likes

I hate to follow up my own post, but I’ve just thought up a counter-proposal and it seems better to post it here rather than start a new thread...

As I’ve already stated, my main concern with SE-0279 is that it adds a new kind of {}-delimited block that:

  1. Is disjoint from existing uses of {} delimiters in the language
  2. duplicates some, but not all, of the functionality of ()-delimited argument lists
  3. does not generalize or extend the existing single-trailing closure syntax

After ruminating on it for a bit, I think all three of these concerns can be alleviated by generalizing the syntax in SE-0279 to cover more cases.

Currently there are two syntaxes at call sites, which I’ll paraphrase as:

<call> ::= <func> <primary-argument-list>

<call> ::= <func> <opt-primary-argument-list> { <closure-body> }

Here primary-argument-list refers to the ()-delimited arguments, which are optional in the trailing closure case.

SE-0279 proposed to add a third case, so we have:

<call> ::= <func> <primary-argument-list>

<call> ::= <func> <opt-primary-argument-list> { <closure-body> }

<call> ::= <func> <opt-primary-argument-list> { <secondary-argument-list> }

Here secondary-argument-list refers to the label: { ... } closure arguments allowed by SE-0279.

What occurred to me is that we can conceivably combine the two cases with a trailing {} into a single more general syntax:

<call> ::= <func> <primary-argument-list>

<call> ::= <func> <opt-primary-argument-list> { <opt-secondary-argument-list> <opt-closure-body> }

In this case the {} can hold either, both, or neither of a secondary-argument-list and closure-body.

When the {} is empty or only contains a closure-body the result should be equivalent to current single trailing closure syntax.

When the {} only contains a secondary-argument-list it should be equivalent to SE-0279.

In the new case when the {} contains both a secondary-argument-list and a closure-body the semantics would be that of passing the secondary arguments followed by the closure defined by the closure-body (which would follow the existing single trailing closure rules of allowing the label to be elided).

I would also argue for generalizing the secondary-argument-list to support arguments of all kinds (not just closures) and use the same whitespace rules already in place for separating statements to know where to separate them.

Under this model, the following would all be equivalent:

// existing ordinary call
f(a: x, b: { y }, c: { z })

// existing single trailing closure call
f(a: x, b: { y }) {
    z
}

// proposed SE-0279 call
f(a: x) {
    b: { y }
    c: { z }
}

// newly enabled cases:

f(a: x) {
    b: { y }

    z
}

f {
    a: x
    b: { y }
    c: { z }
}

f {
    a: x
    b: { y }

    z
}

I believe this generalization addresses my main concerns with SE-0279. The new use of {} is now an extension of the existing trailing closure syntax and can be used together with the existing syntax. Allowing arbitrary arguments (not just closures) in the secondary argument list makes it more comparable in utility to the primary argument list, and the feature thus feels general-purpose.

The main down-side I see is that this generalization provides even more ways to express a given call, so any concerns about making the language more confusing to read or learn are arguably made worse.

Flutter would be the obvious example since the whole Dart language (in its new incarnation) was built just to be a SwiftUI-like DSL. It looks exactly like Swift without any extra sugar. If Swift were to go that route it would need formatters optimized for this. Flutter doesn't use curly brackets in builders though, so they don't have the exact issue since it is all parenthesis. Layouts in Flutter | Flutter

2 Likes

I think we need parentheses to indicate that this is a function call. E.g.

f() {
    a: x
    b: { y }
    c: { z }
}

... and I'm not sure why this would be better than:

f(
    a: x,
    b: { y },
    c: { z }
)

[Edited to add commas]

1 Like

That is explicitly not the case with existing trailing closure syntax and SE-0279, so I’m not sure what would make the constraints different for a generalization of them.

One obvious point is that your second example isn’t legal Swift today because you left out multiple required commas.

I’m not equipped to argue the merits of {} as a delimiter over () for these calls, but plenty of people in this thread have argued that it is their preference.

2 Likes

Good points. I've got used to single trailing closures, and I read them without difficulty, but I'm struggling with multiple trailing closures. Calling a function with:

f  {
   a: x
}

instead of

f(a: x)

looks odd to me. I think it makes Swift more difficult for humans to read.

1 Like

The other thing that makes this hard to assimilate is that there is a parallel between closure bodies and function bodies (both enclosed in braces). Having braces that don't serve the same purpose after a function call is confusing to me. Perhaps I would get used to it, but it seems like additional complexity.

[That's a general comment on syntaxes that uses braces to enclose multiple trailing closures, not just on your suggestion.]

1 Like

FYI- The current direction of this thread is looking at it like this without the curly brackets: (although this isn't a great example)

f a: x b: { y } c: { z } // No idea if the `a: x` would actually be allowed if this were re-proposed.
// instead of
f { a: x b: { y } c: { z } }

You will need to read back to Chris_Lattner3 and xwu's posts for better examples. The syntax in the actual proposal (which looks like the sample in your post with curly brackets) isn't being actively discussed since almost everyone is against it.

Yes, this is all very confusing since this turned in to an alternatives discussion.

EDIT: Clarified

1 Like

That example is truly horrible, especially in its original form:

f a: x

instead of f(a: x)

I think I would prefer a rule that function calls should be followed by parentheses, unless they are immediately followed by a single trailing closure (without a label).

Otherwise, we could presumably call functions that don't have parameters like this:

f

instead of:

f()

and it's really not obvious (to me) that a function is being called.

Yes, but mostly because it is a bad example. Chris Lattner and xwu have better explanations on why this is better than what was proposed. It closes an ambiguity, allows function builders to separate from configuration parameters, and allows a control-flow style in some contexts.

I think the only alternative to this would be to improve Xcode auto-formatting so it actually works for SwiftUI, but there are other issues with that. However, improving formatting could allow this to be punted in to the future when Swift isn't adding major features and sugar extensions are easier to reason about.

Could we have parameter-less labels inside parentheses, as a clue that there is a trailing closure? E.g.

f(a:, b:)
   a: { x }
   b: { y }

would be an alternative to:

f(a: { x }, b: { y })

and

f(completionHandler:) {
    x
}

would be an alternative to:

f {
   x
}

This is good. I like it because it's a cogent statement of a limiting principle. Now let's consider how the proposed design fares in terms of this principle:

Consider the following function:

func foo<T>(
  _ bar: () -> (),
  baz: Bool,
  boo: (T, T) -> Bool,
  baz2: Bool = false,
  boo2: (T, T) -> Bool)

If the proposed design we are given here is used as a tool to emphasize certain arguments as "body" and others as "not body," then we have a few problems:

  • boo2 can only be "emphasized" if we're passing a closure expression; one cannot use a key path literal, or refer to a function by name (boo2: +).
  • baz and baz2 can never be "emphasized" because those arguments can't be written as closure expressions.
  • boo can only be "emphasized" if we're passing a closure expression and only if baz2 is false!

These would be rather arbitrary limits. Rather, it would seem that @Chris_Lattner3's proposed approach answers your limiting principle with full generality:

3 Likes

Fair enough -- I'm only speaking from my own experience. What I should have said is that the formatting isn't palatable to everyone, so it's not a suitable replacement for the proposed syntax for everyone.

This is a great summary of the different ways the counterproposed syntax can be formatted. It's true that all of these examples should format properly with existing tooling. I'm not very motivated by the control flow aspect of the proposal in general, so I don't have strong feelings about that either way. But the last example of not indenting the trailing closures at all really doesn't sit right with me. I think it's important to indent at least one level to indicate the relationship between the function call and the closures. That seems to have been the general sentiment of the thread as well -- I don't think I've seen anyone avoid indenting the closures in their examples. Anyone who wants to indent the closures will have problems with tooling.

Looking at this example, I think I agree now that sticking consistently to a single level of indentation would work best. And it's true that it doesn't make sense that the modifier calls would be chained off of the last closure, so you'll be able to deduce what's actually happening. But that's part of why it's off-putting -- it still looks like you're chaining off of the closure. So I guess I don't find this example particularly difficult to read (though it might be if the closures were longer), but I feel like I have to translate it in my head to see the structure that's not in the code.

I'm not really looking at the proposal this way -- I don't think the lack of commas between the multiple closures is a hugely important aspect of it. I prefer them not to be there, but I would still support the proposal if they were. SE-0257 being accepted or not isn't relevant from my perspective.

I see this proposal as a logical extension of the existing trailing closure feature, the limits of which are in the name -- closures at the end of a parameter list. I don't agree with the idea of allowing braces to be used around the entire parameter list. The benefit of that is better solved by SE-0257 imo, but it's a different problem than this proposal sets out to solve, so I don't think it needs to be fully examined here.

You don't have to use the new syntax for single-line closures. I agree that it doesn't look good -- it is designed for multi-line closures after all. If you want the label for clarity, you're better off not using trailing closures at all in this situation. I don't agree that this proposal needs to address this issue.

On this point, I still feel strongly that the counterproposal is significantly more disruptive and divergent from existing syntax. Nobody has addressed the question I put forth in my previous post -- what existing syntax can the counterproposal be compared to that qualifies it as familiar or natural? People keep saying this, but I find that exactly the opposite is true.

2 Likes

Chris Lattner answers this better if you look back in the thread, but basically: The biggest reason is there would be 3 different syntax representations instead of two: regular arguments, unlabeled trailing closures, and labeled trailing closures. This counter proposal would have two– regular arguments and labeled trailing closures with unlabeled closures being a special case of the later. The proposal would add yet another layer of curly braces and another layer of indentation. The goal is to get rid of braces. The syntax in the proposal would be more likely to affect future language features. Sugar extensions to the language have a high bar since not all of the features planned for Swift are implemented or fully designed yet. The counter-proposal is unlikely to affect any future feature that may be introduced since it is close enough to existing syntax not to get in the way. In the case of just resolving the ambiguity in the language, the proposed syntax would look silly: foo() { action: { x in print(x) } }. That is a lot of curly braces for a single closure.

The counter proposal (and the proposal I guess) look a lot like Microsoft XAML if you are looking for something to compare to. XAML also has two separate ways to represent every argument. One more appropriate for configuration and the other for content, but you can freely switch between the two.

If you are talking about what Swift syntax, the counter proposal is a mix between regular arguments and current single trailing closures.

But again, the proposal cited this as one of its motivations, so you cannot simply discard it because it's inconvenient now (since you also believe it doesn't look good—thank you for that). That's trying to have it both ways.

Well, I can't address it because I don't agree with the premise that "minimally disruptive and has the smallest delta" must also imply that it adheres closely to "existing syntax." I've offered the counterpoint elsewhere in the thread that adhering to existing syntax just because that syntax exists for different unrelated constructs (e.g., property accessors) in the language is not reason alone to use that syntax.

What I mean by minimally disruptive and smallest delta is this. The lack of labels on trailing closures has been mentioned upthread by members of the Core Team as something they feel is lacking in the language. If we set aside the multiple closure case for a moment, how might we fix that? In other words, how might we take a function call with a trailing closure:

foo(someArg: 5) { $0.someProperty }

and add the label back to it, while keeping it a trailing closure? One obvious solution is to simply add the label before the closure. That's the minimal delta—a single insertion:

// Example 1
foo(someArg: 5) someLabel: { $0.someProperty }

I don't believe adding additional grammatical structure where none is needed would be recommended here:

// Example 2
foo(someArg: 5) { someLabel: { $0.someProperty } }

So, hypothetically if we adopted Example 1 and now had labeled trailing closures, how would we extend that to support multiple trailing closures? The minimal delta is to simply have a sequence of them—again, a single insertion for each closure:

foo(someArg: 5) someLabel: { $0.someProperty } someOtherThing: { $0.blahblah }

Again, I don't believe the additional structure provided by another layer of braces is necessary here. It doesn't fall out naturally as a consequence of starting at single trailing closures and generalizing it to multiple closures.

It sounds like you're working from a point of view where multiple trailing closures are the starting point and whatever happens to single trailing closures are side effects. I disagree with that approach. As the discussion in this thread has evolved, I've come to the belief that we need to consider the entire problem space holistically.

5 Likes