SE-0279: Multiple Trailing Closures [Amended]

From my perspective, it's largely a misuse of the syntax to only pop out of the parenthesis an arbitrary subset of the trailing closures. I think it would be a mistake for us to weight this consideration too heavily.

The API guidelines emphasize clarity at the point of use, in particular:

When evaluating a design, reading a declaration is seldom sufficient; always examine a use case to make sure it looks clear in context.

I think it's important to go one step further and focus on "real-world" (or at least "realistic") uses cases. The striking thing about the APIs I found in my research was that they tended to have a "primary" closure, which didn't require an argument label in order to be clear (at least in my opinion):

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

Props to @ricketson who was the first one (AFAIK) who observed this. An animations argument label would be totally redundant:

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

... making it a violation of the API naming guidelines:

  • Omit needless words. Every word in a name should convey salient information at the use site.

Combine has an unlabeled sink variant:

ipAddressPublisher
  .sink { identity in
    self.hostnames.insert(identity.hostname!)
  }

... and SwiftUI has an unlabeled Section variant:

Section {
  // content
}

The authors of these APIs established names and conventions to make the role of these closures clear without a label. Appending additional labeled closures does nothing to make the initial unlabeled closure less clear.

I see the appeal of the order of the closures matching the vertical display order for SwiftUI's Section. Whenever possible we tried to craft the SwiftUI APIs to evoke the UIs they represented and this is in keeping with that.

However, I'm wary of us over-fitting to SwiftUI's Section use case. It's my intuition that, most of the time, the label on the first closure would just be a "needless" word.

ipAddressPublisher
  .sink { identity in
    self.hostnames.insert(identity.hostname!)
  }

ipAddressPublisher
  .sink receiveValue: { identity in
    self.hostnames.insert(identity.hostname!)
  } receiveCompletion: { completion in
    // handle error
  }
3 Likes
  • What is your evaluation of the proposal?

+1. I’m happy with where this ended up.

I do think we need to followup with a proposal to fix standard library API which will no longer meet the revised API Design Guidelines (such as Sequence.first(where:).

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

Yes, functions that take multiple closures have always been a clunky aspect of Swift’s syntax.

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

Very much so.

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

A quick read.

1 Like

I appreciate the pushback; what I don't see is the evidence why the first closure should be the one with the needless word, if there's more than one. That's probably true if there's one mandatory closure argument and one or more defaulted ones, but I'm not sure it's true for multiple mandatory arguments, or all defaulted closure arguments. Of course this does become a new guideline, but that doesn't mean it won't be at odds with other guidelines, such as today's "put trailing closures last" overriding "put [non-closure] default arguments last".

…I'm honestly not sure if that last sentence means my concerns are well-founded or unfounded.

P.S. I wish we could take @mayoff's restriction "you can't just use some trailing closures" to keep the number of possible call sites to two if the naming guidelines don't change, but that would break existing code.

5 Likes

We definitely can. I doubt we ever will. The two design do differ enough that, whichever one we adopt, the amended guideline (and the soon followed APIs) will be driven by the syntax. At that point, the other option will become very unappealing.

If we omit the first label, Section would look one way, if we require it, it would look another.


That’d be the problem in our debate. We can only assume what the majority looks like without much of a cold-hard-data. I speaks of SwiftUI only because that’s the framework I’m most familiar with utilizing calls with multiple closure, while yours UIKit. Since I can easily grab Binding(get:set:) to be the opposite.

2 Likes

+1

I've been thinking the same thing. This is really unclear:

text.drop { $0.whitespace }

It might be worth moving the argument label into the base name:

text.dropWhile { $0.whitespace }
text.dropWhere { $0.whitespace }
1 Like

or...

text.drop while: { $0.whitespace }

:wink:

9 Likes

Assuming you're not advocating for all single trailing closures to start requiring argument labels, my problem with this spelling is that it leaves it up to the user to decide on a method by method basis whether the argument label is required for clarity.

I think if the argument label can be dropped, the API author needs to assume it will be dropped, and name the method accordingly.

3 Likes

The prominent examples I found in the SDK (UIKit's animate, Combine's sink, SwiftUI's Section) were all naturally modeled as one mandatory closure and one or more defaulted ones. Though @Lantua pointed out Binding(get:set:), which is an example of multiple mandatory closures.

My intuition is once you start to have multiple mandatory closures, it's worth considering whether you might be better off declaring a protocol, Binding(get:set:) notwithstanding.

+1

Hmm, could you explain more about this? IIUC if it is the first label is not dropped, it’d then be strictly additive and so be source-compatible.

FWIW, I don’t find while to add much clarity, so I’d probably drop them. Though I can probably recite stdlib at this point, so it’s somewhat biased.

Overall I like it but I am not liking the labels using the colon. I feel that the first label should be required while perhaps the first label can be ignored using _

I think the problem is significant to warrant a change in swift. The syntax seems a little forced but it does flow much better than last proposal.

Has there been any thought about using a pipe instead for the label.

foo() 
| bar { provideBar() } 
| baz {  provideBaz()  } 
| garply { provideGarply()   }

Reference for pipe. Sure it’s a different domain but feels better to me than the colon.

1 Like

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