SE-0279: Multiple Trailing Closures [Amended]

I was wrong. I thought there was a more concrete reason in the proposal. They mention function names and argument labels juxtaposed without parenthesis is unsettling. I suppose that could be corrected by requiring parentheses in this case. I think the real reason is they want to introduce the most minimal amount of new syntax since the goal is to avoid new sugar-only features until all major feature manifestos have concrete implementations.

That wouldn’t work since there is still the desire to use it in a parameter list sometimes and it would be source breaking change.

It feels a little forced only because it is a minimal implementation for immediate needs. It could be fleshed out more over time. Some of those ways are detailed in the proposal. I think it is best not to move too quickly on this since implementing DSLs in Swift is still evolving. Just doing the minimum for now to help the needs of SwiftUI and Combine feels right to me. I think there is a clear paths forward beyond what is proposed here. The original thread talked this to death.

This would be ambiguous with operators, function names, keywords, and variables.

1 Like

I wish formatting guidelines would be an important part of such a proposal.

I regret the proposal does not respect the default formatting used by Xcode, the tool used by the majority of developers targeted by the technologies used to support for the proposal (UIView, and SwiftUI), in order to help the readers who are kind enough to evaluate the proposal.

3 Likes
  • What is your evaluation of the proposal?

I'm not opposed to it but I don't think is worth it.

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

No. This is where I disagree with the basic premise that is a problem worth solving.

IMO the problem with trailing closures is intrinsic to them. It's a great feature for single closure functions but if you have more then it's problematic (as we know), and I don't think (after seeing all alternatives proposed) that problem can be fixed with more syntax.

The proposal says that with this change the calling site is more readable, and I still disagree. With more than one closure there is nothing more readable than using normal call syntax and specifying all arguments. We do that with every other parameter so I don't think is such a big deal to do it for closures too. Is the beauty of having a language with labels in the parameters.

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

The proposal in its current form matches more the feel of the language than its previous incarnations, so if it eventually gets accepted, I prefer this version than the previous ones.

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

I've been following the multiple threads that spawned around trailing closure syntax for a while so I've seen all the alternatives and discussions.

8 Likes
  • What is your evaluation of the proposal?
    -0 I strongly disliked the original proposal, this one is better but I still don't like it

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

  • Does this proposal fit well with the feel and direction of Swift?
    I'm fairly neutral on this proposal and I'm convinced that it has already been decided that this proposal fits the direction of Swift.

  • 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 followed and reviewed the original proposal and reviewed this one by doing a good read of this update

3 Likes

I don’t think we should expect Xcode to do a good job with formatting of syntax that does not yet exist in the language. Further, Xcode does not do that great a job of formatting in general. Proposals should not be expected to look nice given any specific formatter, only to be amenable to clear formatting.

If formatters struggle with the language they should be improved. That is (currently) outside the scope of SE. Anyone interested in improving formatting that comes out of the box should review the epic discussion that was had on the topic as well as the core team’s feedback and start new discussions on the topic in the development section of the forum (as the core team requested).

2 Likes

You're right. But this situation is far from ideal for proposals of this kind. Some people here may end up fostering their future pet-peeve, without any recourse. This is a problem.

I hope the core team will bring back the subject of formatting on the table very soon and very seriously, even though the end result may turn out as brutal as the transition to Swift 3, and that such proposals have to contain a formatting section, and an implementation for the formatter as well.

End of the "meta" topic, sorry for the interruption.

1 Like

Moderation note

While I agree, this is really a separate discussion from the review, so I've started a thread to discuss this particular issue. Please discuss the potential changes to the standard library there.

It's a fine line, because discussion of the rules for multiple trailing closures is perhaps helped by considering the current challenges of labels with trailing closures. But specifically addressing what exists in the standard library should go in the other thread.

3 Likes

I tried to play around with some extensions that I'm currently using, and how they could change if this is implemented.

extension Optional {
  func forEach(_ body: (Wrapped) throws -> Void, ifNone: () throws -> Void = {}) rethrows {
    if let self = self {
      try body(self)
    } else {
      try ifNone()
    }
  }
}

let x1: Int? = 42

/// This is great
x1.forEach {
  print($0)
} ifNone: {
  print("nihility")
}

extension Collection {
  func forEach(_ body: (Element) throws -> Void, ifEmpty: () throws -> Void = {}) rethrows {
    guard !isEmpty else {
      try ifEmpty()
      return
    }

    for element in self {
      try body(element)
    }
  }
}

let x2 = [1, 2, 3]

/// This is amazing, but using `forEach` would require a change in the standard library
x2.forEach {
  print(0)
} ifEmpty: {
  print("emptiness")
}

extension Result {
  func fold<A>(onSuccess: (Success) -> A, onFailure: (Failure) -> A) -> A {
    switch self {
    case let .success(value):
      return onSuccess(value)

    case let .failure(error):
      return onFailure(error)
    }
  }
}

let x3 = Result<Int, Error>.success(42)

/// This is ugly, it's not clear without the first label
let x4 = x3.fold {
  $0 * 2
} onFailure: { _ in
  0
}

/// This is better, and it's the current syntax.
/// It should be preferred when the method name is too generic sounding, and argument labels give the appropriate meanings to the closures.
let x5 = x3.fold(
  onSuccess: { $0 * 2},
  onFailure: { _ in 0 }
)

Overall, this seems excellent. I'd probably review some current APIs to better accomodate this proposal, as already mentioned, but it's a separate discussion. The glorious when example should be, for me, an instant addition to the standard library.

I think this is definitely in line with the direction of Swift, and the additional convenience seems to warrant this change.

I could imagine myself to prefer this form if it was allowed.

result
  .fold
    onSuccess: { $0 * 2 }
    onFailure: { _ in 0 }
  .distance(to: 42)
6 Likes

This is a good example where the closures really are peers. Not supporting a label for the first closure in this case would be actively harmful.

If we need to support it in some cases then change to the API Design Guidelines be revised:

Name functions assuming that trailing closure syntax will be used. Include meaningful argument labels for closures when their meaning is not clear from the base name alone.

If we go in this direction, maybe we should consider deprecating trailing closure label elision altogether. This is a much larger change to the language, but would align declaration with usage and ensure all call sites are consistent.

The misalignment of declaration and usage could be considered actively harmful in the presence of an alternative that makes the language more consistent. For example, usage sites such as array.first { ... } require the reader to know more about first than should be required.

I'm not sure what migration might look like, but perhaps static analysis could infer whether a label should be required or not from usage sites.

12 Likes

I haven't done a deep review, but I agree with Jordan and others who say that we shouldn't provide special behavior to the first closure parameter just because it was first. All closure should have to respect the labels provided by the API author.

Now, the proposal provides parsing and grammar rules for the compiler to be able to parse peculiar expressions of this logic, but that doesn't help users at all, especially when the parameters are unlabeled or defaulted.

func pointFromClosures(x: () -> Int = { 0 },
                       _ y: () -> Int = { 0 },
                       _ z: () -> Int = { 0 }) -> (Int, Int, Int) {
    (x(), y(), z())
}

Now what does this mean:

pointFromClosures { 1 }

If you're familiar with the peculiar rules of this new multiple closure behavior, you'd know that it's x getting the value there. However, if you have any previous Swift experience, you might assume it's z, as the trailing closure, that gets the value. I think this is a problem: this new syntax is incompatible with current Swift behavior while appearing the same.

Another problem:

pointFromClosures
    _: { 2 }

Which closure gets the value 2, y or z? If you following the parsing rules laid out in the proposal you'd know it was y, but most users won't know that. So with these new complex rules, there's little the user can do to intuit a correct result.

And a final issue:

pointFromClosures {
  1
} _: {
  2
} _: {
  3
}

This just looks bad. I think full excision of labels looks better.

pointFromClosures
{ 1 } 
{ 2 }
{ 3 }

Ultimately, though, much of the issue here was just bad API design, so lets assume we fully labeled the closures:

pointFromClosures
{ 1 } 
y: { 2 }
z: { 3 }

Like @anandabits said, this looks odd as well, since the values are peers and should have the same labeling requirements.

Finally, I take issue with this text in the "Alternatives Proposed" section:

While this syntax is clear at the point of use, pleasant to read, and provides contextual cues by separating the trailing closures from the rest of the arguments, it risks evolving into an alternative calling syntax. The proposed syntax is more concise and less nested, without loss of clarity:

UIView.animate(withDuration: 0.3) {
  self.view.alpha = 0
} completion: { _ in
  self.view.removeFromSuperview()
}

We have a new topic talking about this precise issue, so I don't think you can argue there's no loss of clarity: you'd have to understand what the first closure does in the animate example just like in the count example from the other topic. It's been brought up earlier in this topic, but I found the responses to be less than satisfying.

Overall, I'm not sure this proposal goes far enough to distinguish between closures of relative importance, fully solves the usability issues around multiple trailing closures for typical users, or gives API developers the tools necessary to guide such users toward the intended call sites.

16 Likes

It is in fact z that gets the value, and then the call doesn't type-check. Also it's impossible to change this without a source change because this is currently-supported syntax. The proposal does go into this.

This isn't legal under the proposal.

Ah. I think this makes it worse then, as it would provide two completely different unlabeled trailing closure rules.

Which part? Multiple unlabeled closures?

2 Likes

You’re literally pointing at the current Swift behavior and saying “this makes it worse”. I think you’ve just misunderstood the proposal.

Labelling the first trailing closure in a call.

I realized this doesn’t quite capture what I intended. I didn’t mean to imply that the _: “placeholder” label would be required for the first trailing closure argument.

2 Likes

Ah yes, I see. I wouldn't be able to use the trailing syntax to set just the y or z (or y and z) closures, in that case, would I?

Correct, the proposed syntax does not allow you to be explicit about which parameter you're intending to supply, in cases where they're all defaulted. This API, if it existed, would match poorly with this proposal.

-1. More complexity and more special syntax is a bad thing in my opinion. Swift seem to be ever-expanding into having more and more stuff in the language.

No, it's just more syntax and options in an already very complex language.

Personally, I wish swift would have gone the opposite route: making trailing closures an opt-in feature for functions, so that only functions for which it makes sense (e.g. DispatchQueue. and friends) could this special syntax.

No comment. It's hard to say what the feel and direction of Swift actually is with so many moving parts.

N/A

Followed all the threads.

4 Likes

No, I'm saying the more I learn about the complexities this proposal brings to the considerations users have to make around trailing closures, the "worse" I think of it. Swift will now have the following rules, if I'm understanding correctly:

  • Single closure at the end of a function, you can:
    • Use it labeled.
    • Use it with trailing syntax.
  • Multiple closures, all at the end of the function, you can:
    • Use them all labeled.
    • Use all but the last labeled, use the last with trailing closure syntax.
  • Multiple closures, all at the end of the function, with the new syntax, you can:
    • Use them all labeled.
    • Use all but the last labeled, use the last with trailing closure syntax.
    • Use the first as a trailing closure.
      • If the rest of the closures are defaulted, it looks like typical trailing closure usage.
      • If any are not defaulted, you must use the trailing syntax first, then add labels for the additional closures.
      • If multiple defaulted closures are unlabeled you can't disambiguate between them and so can't use the trailing syntax at all. Though this is also true when using them inline (I think), and so may be a bad example.

I really think it would simplify the user's consideration here to make the first closure elision contingent on not having an external label. That way API designers could have some control over this form and not have to move things around for the multiple closure case. Take this (fake) API for something like Alamofire (types elided).

func request(..., modifier: {}, uploadProgress: {}, downloadProgress: {}, completion: {})

In this function, completion is the most important, and likely the only one not defaulted. However, in trying to use this function with the trailing syntax I'd be forced to either create an alternate form where completion is first, which is rather illogical, or use requestModifier as a trailing closure:

request(...) {
    $0.timeoutInterval = 5
} completion: {
    ...
}

Instead, it seems like, by having an external label on the parameter, I'm saying that users should label it, if it's used. We do lose any name elision in that case, but I think that's okay.

request(...)
    requestModifier: {
        $0.timeoutInterval = 5
    } completion: {
        ...
    }
12 Likes