SE-0279: Multiple Trailing Closures

I had this idea in an earlier post, but it was an unintelligible brain dump. I hope this is easier to follow.

The idea is very similar to the alternative proposal, but approaches it from another angle. I came to the realization that the alternative proposal felt like a function builder pattern on the function call. Function calls would conceptually only have normal argument types. The trailing closures are instead thought of as a metaprogramming equivalent to function builders that operate on the function call. I think this may appeal to more people since it does not need to be thought of as a function call with multiple kinds of arguments. Like the alternative proposal, this also solves the ambiguity.

Think of the following example as calling two SwiftUI modifier style metafunctions (called ".#animations:" and ".#completion:") that add the "animations" and "completion" arguments to the end of the "animate(..)" function call. The metafunctions use a different label-like syntax and metafunction composition operator ".#" to differentiate them from normal functions. These builder metafunctions (or label-like metaproperties if you prefer) are formatted with a macro-style syntax similar to literal expressions or conditional compilation blocks since that implies metaprogramming:

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

Button() 
    .#action: {
        doSomething()
    } 
    .#label: { 
        Text("") 
    }
    .padding(20)

This composes well and the flow is easy to read with or without indentation. The view modifier continues to look visually connected to the Button. I assume the metaproperties act like normal properties where the order doesn't matter.

Alternatively, the macro syntax could be dropped since the syntax is already unambiguous:

Button() 
    .action: {
        doSomething()
    } 
    .label: { 
        Text("") 
    }
    .padding(20)

This may be taking the concept too far, but there could be varients of these metafunctions for special cases. Think of .foo: as a metaproperty and .#bar(...) as a metafunction in this example.

// Set arguments by index.
RandomAction(action2: { doSomething2() }) 
    .#action3: {
        doSomething3()
    }
    .#argument(atIndex: 0) {
        doSomething1()
    } 

    // Variadic function
    foobar(action1: { doSomething1() }) 
    .#append() {
        doSomething2()
    }
    .#append() {
        doSomething3()
    } 

This syntax could be expanded to non-closure arguments to help pull large arrays, dictionaries, or verbose function calls out of the arguments list. Ordinarily I would put these awkward arguments in a constant then place the constant in the argument list. This would pollute the local namespace with a lot of constants that are only used once. By expanding this to other argument types, the awkward functions could be specified inline without losing track of parentheses and braces. This could benefit DSLs that use something other than a nested function builder for content. For example, a DSL built as a list of enum values could benefit.

// Note: A good example would be much more complex as this would inline the traditional way reasonably well.

// A constant that is used one time to help reduce nesting
let configuration = [
    .name("My Name"),
    .file("myfile", inBundle: "mybundle")
]
let service = MyService(x: 1, y: 1, z: 1, configuration: configuration)
// Now we can't use the `configuration` constant for anything else in this scope.

// Inline version
let service = MyService(x: 1, y: 1, z: 1)
    .#configuration: [
        .name("My Name"),
        .file("myfile", inBundle: "mybundle")
    ]

This is almost the same as the alternative proposal, but adds a metafunction composition operator ".#" and may be easier for someone to wrap their head around two representations for arguments. Does anyone else feel this is Swifty way to fix the ambiguity and add multiple trailing closures/arguments?

EDIT: Fixed typos– many times.

It‘s an interesting idea. What if we used $ symbol instead and called the trailing closure parameters as 'projected parameters' as they are projected out(side) of the parameter list.

Button() 
  $action: {
    // ...
  } 
  $label: {
    //...
  }
  .padding(20)

If you happen to have a wrapper property nearby with a projected value that would match the label, you could still disambiguate because:

  • the projected parameter label is followed by a colon
  • the projected property can use self. prefix

This can be further generalized to apply to all parameters in the list but require them all to have an label.

Looks like a sweet spot to me, especially because the first introduction to $ prefixed identifiers to the non-evolution Swift community came through SwiftUI during last WWDC.

var body: some View {
  Button() 
    $action: self.$action 
    $label: {
      //...
    }
    .padding(20)
}

Thoughts?

Seems to conflict with usage from property wrappers, despite your attempt to connect the two. I don’t think users will have an easy time using the same syntax to access a value as to mark an external closure parameter.

2 Likes

This thread looks like it doesn't need more details, but:

(actionBitPattern ^ labelMask) &<< paddingBitCount

: )

And I guess I could express my view on this proposal as well: -1, because Swift's syntax is already too complex as it is.

1 Like

Hello,

As an API author myself, I would appreciate that this paragraph would be made very much more explicit, with a comparison of the consequences of the available options. I'm afraid that the partial and potentially erroneous information splattered across this thread could lead to wrong conclusions. This is the difference between thinking and guessing.

2 Likes

Maybe, but I personally kinda like the notion of 'projected parameters'. I don't think there would be any issue from the compilers side, the only thing is yet again users readability. However to me $foo: $foo is good enough to identify what is what.

Theoretically we could already have this situation:

@G
var foo: Int
// var $foo: Int

var dictionary: [Int: Int] = [
  $foo: $foo
]

This example has delimiters, but the original idea would be a projection of a parameter list from a function or an init before its appearance.

This does reduce the extra level of indentation and it borrows the notion of projection introduced with property wrappers. Also it would already use a compiler reserved prefix.

Personally I'm intrigued by that. Other folks are free to disagree. ;)

@DevAndArtist One interesting thing your example points out is that the alternative delimiter-less syntax would effectively preclude SE-0257, at least from dictionary literals:

func foo(a: Int) -> Int { 0 }
func foo(a: Int, b: () -> Void) -> Int { 0 }
let b: Int = 0

let dict: [Int: Any] = [
    0: foo(a: 1)
    b: { print(“Hello!”) }
] // dictionary of one element, or two?

I had to jump back and forth now (you should also link the proposal you mention) to understand what you meant. Since SE-0257 is not yet implemented my syntax and the lastly pitched form from this thread might create a possible collision.

In you example the collision for both the reader and the compiler would be the b: { print(“Hello!”) } line. It is not clear if this a second element of the comma-less dictionary literal (iff SE-0257 was accepted) or if it's a 'projected b parameter' from the second foo overload.

This is a really good observation!


As a possible solution to this problem I propose to use the already pitched idea of also using a leading dot.

Ultimately projected parameters would look somewhat like properties, but since they also must contain a trailing colon it should be enough information for both the compiler and the reader to identify it as a 'projected parameter'.

My example from a few posts above would become:

var body: some View {
  Button
    .$action: self.$action 
    .$label: {
      //...
    }
    .padding(20)
}

This example would be transformed from:

to:

if array.contains.$where: { $0.isABadThing } { ... }

Keep in mind that this way we no longer talk about trailing closures only but can project and trailing sub-set of parameters up until a label less parameter.

func bar(_: Int, closure: () -> Void, string: String) { ... }

bar(42, closure: { ... })
  .$string: "swift"

bar(42)
  .$closure: { ... }
  .$string: "swift"

You might quickly come up with an example where this form hits a small wall as what if the passed parameter also uses 'projected parameter' where he last one would match with the followed projected parameter from the current list.

The solution is simple here. We can use a semicolon to tell compiler our intention.

func brr(x: Int) -> Int { ... }
func brr(x: Int, y: Int) -> Int { ... }

func baz(x: Int, y: Int)

baz
  .$x: brr
    .$x: 0;
  .$y: 42 // this `y` is from `baz` not `brr`

Thanks for summarizing and linking! Unfortunately I'm not sure that resolves the conflict for either proposed alternative in this thread due to the availability of leading-dot syntax (and its compatibility with projected values). Consider:

@propertyWrapper
struct S {
    var wrappedValue: R
    var projectedValue: R = R(x: 1)
}

struct R: Hashable {
    @S static var r: R = R(x: 2)
    var x: Int
}

func foo(a: Int, r: () -> Void) -> Int { 0 }
func foo(a: Int) -> Int { 0 }

let dict: [R: Any] = [
    R(x: 0): foo(a: 0)
    .r: { print("Hello, world!") } // second argument of `foo(a:r:)`, or `R.r`?
]

let dict2: [R: Any] = [
    R(x: 0): foo(a: 0)
    .$r: { print("Hello, world!") } // second argument of `foo(a:r:)`, or `R.$r`?
]

What's the issue in the code sample? I don't understand what you meant with the // comment. Can you elaborate a little more please?

This is the problem I edited into my post at the end, it won't only appear in case of dictionary literals with SE-0257 in mind. I think the compiler should prefer the projected parameter unless explicitly told otherwise. That said, your example should naturally resolve to "second argument/parameter of foo(a:r:)". If you want to resolve the collision you either would opt out of SE-0257 and use a comma or you can use a semicolon.

let dict: [R: Any] = [
  R(x: 0): foo(a: 0)
    .$r: { print("Hello, world!") } // second argument of `foo(a:r:)`
  // notice that we don't need a comma after the above line (SE-0257)
  R(x: 01): foo(a: 1)
]

let dict2: [R: Any] = [
  R(x: 0): foo(a: 0), // explicitly opt out to resolve an issue SE-0257 would cause
  .$r: { print("Hello, world!") } // `R.$r`
  // notice that we don't need a comma after the above line (SE-0257)
  R(x: 01): foo(a: 1)
]

let dict3: [R: Any] = [
  R(x: 0): foo(a: 0); // explicitly opt out to resolve an issue SE-0257 would cause
  .$r: { print("Hello, world!") } // `R.$r`
  // notice that we don't need a comma after the above line (SE-0257)
  R(x: 01): foo(a: 1) 
]

Ah, I'd missed that edit.

I'm not sure I love the "just add a semicolon" solution. Currently semicolons are statement delimiters, and this would introduce a whole new meaning to an already non-idiomatic Swift feature. I think sticking with the rule the the trailing parameters must be closure literals is necessary to avoid a whole lot of confusing constructs. Nested, delimiter-less parameter lists are extremely difficult to read, for me.

Has anyone pitched a leading colon instead of a dot yet?

Button
  :action: { ... }
  :label: { ... }

It looks strange, but it could also maybe do the trick. Well we'll see what the core team decides soon.

My only issue here is that SE-0257 and this SE-0279 will bite each other.

Having some sore of indicator (like .$ or .# or :) would be better than having an unknown number of trailing closures floating around after function calls.

That's one thing the original proposal has going for it, it scopes the argument labels so it is unambiguous who they belong to (and that they are actually trailing closures).

When we look at the single trailing closure syntax, we can clearly identify it as a trailing closure because it does not have enough information to stand as its own statement. Plus, it is literally right next to the function call.

foo(bar) { baz in
  baz.add(3)
}

But if we make the trailing closures labeled (which is necessary for multiple closures) and connect them only by proximity to the previous closure, it becomes difficult to differentiate this

foo(bar) process: { baz in
  baz.add(3)
} 
package: { baz in
  baz.zip()
}
translate: { baz in    // Trailing closure named "translate"
  baz.localize()
}

from this

foo(bar) process: { baz in
  baz.add(3)
} 
package: { baz in
  baz.zip()
}
translate { baz in   // Function named "translate" with single trailing closure
  baz.localize()
}

because once we add the label, we are only one character away from it being a syntactically valid function call itself.

Yes, indentation would help differentiate in this example, but it should not required.

2 Likes

Sorry to say this. But wtf is this thread. This has become a hobby syntax designers wet nightmare.

And no: this is not in reply to one specific post and especially not the previous one : )

6 Likes

I think this really has very little to do with this review; it's about the naming guidelines.

2 Likes

I agree that this thread has gotten out of hand. The review period ended a few days ago, and the Core Team is reviewing the feedback it's received; thank you all.

17 Likes