Pitch: Double (or more) trailing closures

Yes you're absolutely right, thanks for pointing this out, I'm starting to think the best option is what I proposed in the comment before yours (ie: only allowing else, catch, finally etc.) as labels.

requests.get("/user?ID=12345") { response in
    let user = response.args["user"]
    ...
}
catch { 
  // do something else
}
finally {
  // do something more
};

This should cover 99% of the use cases without the ugliness/parsing issues introduced by allowing arbitrary labels to act as "pseudo-keywords".

A simple solution to the ambiguity problem of multiple ‘trailing’ closures is to simply require that they start on the same line as the preceding closure, after the }. e.g.:

// This is a single call to when(_:_:otherwise:)
when (2 < 3) { doSomething() } otherwise { do SomethingElse() }

// This is two independent calls, one to when(_:_:), the other to otherwise(_:).
when (2 < 3) { doSomething() }
otherwise { do SomethingElse() }

Or if you prefer:

// Again, unambiguously all a single statement & function call.
when (2 < 3) {
  doSomething()
} otherwise {
  doSomethingElse()
}

// Two independent statements & function calls (though it would arguably
// be stylistically superior to put a blank line between them, for further clarity).
when (2 < 3) {
  doSomething()
}
otherwise {
  doSomethingElse()
}

This builds on the notion that Swift implicitly uses line returns to signal the end of a statement, in lieu of an explicit semicolon. It also still leaves you the option of using an explicit semicolon to differentiate the scenario where you really do mean to start a second statement, not pass an additional, optional argument.

The compromise it makes is that it rules out the style choice of placing line-returns after the }, but I feel that’s fine since that’s atypical style in Swift, and if it comes down to a choice it’s more in line with idiomatic Swift than requiring special look-ahead parsing, semicolons on argument names, etc. It also ensures you can precisely mimic existing control flow syntax, if that’s your thing.

Mechanism aside, and thinking about whether Swift should adopt such functionality… I am intrigued by this. It does have some obvious concerns that would need assuaging, such as that it won’t end up being a C++ templates situation where people get carried away and produce confusing pseudo-DSLs and generally over-designed monstrosities. I’m also not all that interested in the intellectual amusement of replicating existing control flow structures by other means - that’s cute, but doesn’t seem practical.

However, I am interested in it being a cleaner way to express the very common patterns involved in using futures & promises, asynchronous / conditional control flow, and “N+1” call sequences (i.e. call this first closure for each of the N iterations / things / whatever, then this second closure once at the end).

It must be noted however that, unless variadic arguments of closures are also supported, this doesn’t seem to scale as well for … }.andThen { … }… sort of things.

2 Likes

This should be disallowed because it is so unswifty

Forcing trailing closures onto the same line feels like a really strange and non-obvious constraint to place on your code formatting, even though it does fit most of the time.

As much as I would love this feature, I'm wondering if the difficulty in making it unambiguous to the compiler that it's a trailing closure rather than a separate method call is going to cause similar issues for the user. Syntax highlighting that can make it more obvious but even that would require a decent amount of thought.

There are quite a few places in the standard library source code where they use a style of

if {
  ...
}
else {
  ...
}

and this is a configurable option in swift-format.

SwiftUI also seems to encourage a line break after the } when adding modifiers, although the situation is different since those are preceded by a dot:

Button {
  ...
}
.padding(...)
.background(...)

So this isn't a safe assumption to make, and if the goal is to allow people to write closure-based APIs that look like built-in constructs, the following inconsistency is a hard pill to swallow:

// Compiles
if { ... }
else { ... }

// Doesn't compile
myCustomThing { ... }
followupClause { ... }
2 Likes

No one is addressing what I proposed a few comments up. I'd like to hear thoughts on whether that would be better or not. If people feel that else, catch, finally are not enough maybe a select few keywords could be reserved for this use (andThen, etc.).

I'm starting to think the best option is what I proposed in the comment before yours (ie: only allowing else, catch, finally etc.) as labels.

I'd say that restricting them to keywords is awfully limiting. There are also other usages like multiple callbacks (alter values, cancel), which don't easily have keyword counterpart.

1 Like

I’m not clear on when these blocks would be executed, save finally obviously being executed last and always. Does the return value of the first closure need to be a Bool for an else block to be conditional on it?

It is, but it currently looks like the only way to avoid parsing issues, syntax workarounds and the strangeness of having things that look like keywords but aren't. We could get around that limit by carefully picking a handful of select keywords to add to the language that have no meaning on their own but are reserved to be used as labels for multiple trailing closures.

I can see that it would be hard to agree on a list of keywords to add that is both short, generic enough to cover most use cases we can foresee while not being something you'd usually pick as a function name. It might still be worth considering.

Well that would be up for implementation and as long as it's reasonable then it would be understood. For example, we already have another use for else in guard statements, but we can still understand what it does because it's consistent (it runs when the condition is false). I would expect further uses of else to be consistent with this, so returning to your question the first closure would never be called (unless by first closure you meant the condition auto-closure, in this case I'd expect the return to be a bool but it's not something that should be enforced).

If there were a small, fixed number of words we can use I'm afraid that we would end up like c++ operators that use bit shifting for i/o

5 Likes

You're referencing operators, which in languages like Python are a small fixed list and yet they aren't abused like in C++. That's probably more of a matter of culture than anything else.

I do agree that the list can't be small enough to be limiting or people will shoehorn something into the wrong one, and it needs to cover almost all use cases without being big enough to be annoying by taking too many words out of the language. It's difficult, but we already established that this is mostly useful for things that act like callbacks, promises or conditional statements so maybe a small list of keywords that describes almost all these uses isn't out of the realm of possibility and shouldn't be dismissed before trying.

1 Like

We don't really need to push this so hard to the point of limiting ourselves. I don't think it will lead to a good design. I also agree with @cukr that people will get really imaginative with the limited choice they have (how-much-ever we tried to accommodate them all).

IMO, things like onClick onDeselect would also be a potential usage. Even lightweight delegate (aka, a bunch of closures) will do.

This is also be good alternative once we have function builder:

func setup(callbacks: @CallbackBuilder () -> (onClick: () -> (), onInactive: () -> ()))

setup {
  // This is a function builder
  onClick {
    ...
  }
  onInactive {
    ...
  }
}
5 Likes
Terms of Service

Privacy Policy

Cookie Policy