[Accepted] SE-0279: Multiple Trailing Closures

But it really shouldn't. There are mistakes that get still shipped. SwiftUI and Combine had plenty of them those last year and it took almost a whole year to fix them (wrong property names, wrong initializer names, etc.). These are similar API issues, which later on required solutions such as @_alwaysEmitIntoClient to back port the correct API's.

3 Likes

This is a common and important use case.

I appreciate this proposal is attempting to tackle a long-standing limitation with the language, that's never going to be an easy task to undertake. But this proposal accepted as-is hasn't managed to tread the line of brevity for me; the grammar is attempting to be too succint such that it impairs the ability to read the intent of callers without going down the rabbit hole of looking up the function signature/IDE assistant.

3 Likes

For my part, I think this would be fine as it is. If I see a use site like:

return myResult.fold {
  Optional($0)
}
  onFailure: {
    print("Error: \($1)")
    return Result.success(nil)
  }

I'm going to assume that the unlabeled block is the success case, because (a) that's the only thing that contrasts with failure and (b) it's the more "common" case anyway.

For the same reason, if I see:

Binding { foo.getThing() }
   set: { foo.setThing($0) }

I'm going to assume that the unlabeled block is the getter—it's (a) the only thing that makes sense next to set, and (b) the more frequently called, more "basic" case. (That doesn't mean it's good style, of course, but I don't think it's going to be terribly confusing either.)

(Aside: There's no language rule requiring get to come before set in a computed property, but how often do you think people write set first? I'd wager very rarely, which is kind of interesting to realize.)

2 Likes

I'm not saying that's good or should happen, (though isn't the point of the beta to avoid releasing those mistakes?) My point there is if Apple was to strong-arm the Swift Core team into accepting a proposal, they would do it for the release version, not for the first beta.

1 Like

Sorry if this sounds a bit rude, but since when the API guidelines are prioritising assumption over better clarity at call site? Was the argument label for the first closure redundant?

7 Likes

I'm sad that this decision is being made in such a rush time, just in time for the WWDC, when we aren't really close to solve a much needed and wanted features like function builders, which are mostly dependant on other much needed language feature that is covariant generics. With all this recap, I just feel that this change is pushed internally to align with some kind of new APIs being presented with the new version of SwiftUI.

I'm not against the decision itself, I'm disappointed in giving this such an importance that doesn't have in the language when the other ones are very much needed for the language to get at the same level of other modern languages. Even today was presented equatable, hashable and comparable to tuples, five major versions of Swift later!

3 Likes

This comment is almost verbatim what I was going to write out and reply to this thread on.

We absolutely need this as a community for SE reviews, but especially revision reviews, as every time a proposal has been ruled on by the Core Team that goes against the (perceived) community "consensus", we have similar threads discussion the review with the quote "SE is not a democracy".

2 Likes

I think Chris still holds the same opinion as of 1 month ago. He reiterated it in a recent episode of Accidental Tech Podcast. It's at 1:49:38.

1 Like

Hi,
There are some really amazing proposals, and I have myself wished for something like this for multiple closures but that has been usually with UIView.Animation and specifically on the completion bit. Looking at the conversation and various examples, in my opinion if someone was to accidentally not balance the curly braces, they could end up with some interesting side-effects. So I feel this is not such a good idea.

However for multiple closures, ensuring a label for each closure would make a lot more sense and ensure that errors are avoided.

The example,

Binding
   get: { return 42 }
   set: { print("The meaning of life") }

This kind of makes more sense to me than

Binding
  { return 42}
  { print("What is this") }

BUT, in both examples the closures float and thereby make it confusing. So we need to have something that also marks the scope of the function otherwise everything else that follows becomes part of the earlier line of code and could have serious side effects.

This is not making a lot of sense to me unless there are some better examples of usage, thanks

Cheers,

1 Like

You won't get the parameters filled in when you delete that placeholder though, which is why people just hit "Enter" most of the time

Functions like Result.fold or Binding.init have a different story than, say, UIView.animate(withDuration:animations:completion:): the latter conveys a specific meaning of its parameters in the root name, while the formers don't.

As another example consider the Result.filter function, another very important and very useful method on Result:

extension Result {
  func filter(
    _ condition: (Success) -> Bool,
    orFailWith getError: (Success) -> Failure
  ) -> Self {
    flatMap {
      condition($0) ? .success($0) : .failure(getError($0))
    }
  }
}

The word filter already means that we're evaluating a predicate, i.e., a function that returns Bool, in order to accept or discard the wrapped value.

With the new syntax, I'll be very fine in reading this:

let r1 = Result<Int, String>.success(42)

someResult
  .filter {
    $0 > 0
  } orFailWith: {
    "\($0) is not greater than zero"
  } 

But Result.fold and Binding.init don't work the same way. Assuming that one knows what fold means in the context of transforming a data structure, one expects that fold is passed all the individual functions that project one of the possible values into a single return type.

Consider the Inclusive type, that models an "inclusive or" operation at the type level:

enum Inclusive<Left, Right> {
  case left(Left)
  case center(Left, Right)
  case right(Right)
}

folding this would require 3 parameters, thus 3 trailing closures, but none of them is the "main one". Same as Result.fold: the fold operation doesn't consider a "common" case. The idea that I can "assume" that, given the fact that I see onFailure, the other branch must be onSuccess, is hard to digest: it defeats the purpose of parameter labels, changes the very meaning of the root function name itself, and poses a pointless intellectual burden on the API user, because one that knows what fold is about, expects both parameters equally spelled out (I'm remarking the requirement of knowledge of what fold is about because fold, like map, is, of course, not an API based on natural language, but on underlying type theory).

With the accepted design (for which I hope a second amendment will be considered given the overwhelming feedback), fold simply doesn't work with trailing closures, and should be used without them, with proper parentheses and all, which is exactly the way I'm using it right now: the design will not solve the current problem... it will only turn the problem on itself, resulting in another problem.

Binding.init suffers from a similar problem: there's no indication in init that some closure must be the "main one", and "inferring" from set: that the first is, secretly, get:, is bad for the same reasons. Assuming knowledge and understanding of obscure APIs from users is a bad strategy in general, and Swift goes a long way in trying to improve the situation: parameter labels, avoiding parentheses in structured constructs, the various keywords that flow like natural language, et cetera, are all features that improve the code readability and make the language distinguished from some of its competitors (for example, APIs that take multiple closures in Kotlin are terrible to read at call site).

Notice that really all constructors that take multiple closures simply will never work with this design: the only case where a constructor with a trailing closure works as is it's for data structures that wrap the closure itself (like a Func type, that wraps a function). For example, passing some kind of completion closure to a UIViewController constructor in trailing fashion is really confusing. Consider this:

class Page: UIViewController {
  private let completion: (UIViewController) -> Void

  init(title: String, completion: @escaping (UIViewController) -> Void) {
    self.title = title
    self.completion = completion
    super.init(nibName: nil, bundle: nil)
  }
}

let vc1 = Page(title: "Hello") {
  $0.dismiss(animated: true, completion: nil)
}

That Page.init makes no sense, I consider it terrible API design.

The accepted proposal takes a stance against the usage of a parameter label for the first trailing closure: this means that the root function name MUST be phrased in a way that suggests a "main" functionality to be express by the first closure, thus rules off any kind of "symmetric" API. For an example of a "non-symmetric" API look no further than map: if you know what map means, when you read Result.map you know that we're talking about the success value, because map's semantics prioritize a certain path. Other kinds of maps, like bimap, or dimap wouldn't work because their semantics don't assume a preferred path.

My question was: given that fold wouldn't work, what could be a sensible API design that would express the same functionality? In this case in natural language, because fold, map, flatMap et cetera have a specific meaning in the context of specific data structures, and have nothing to do with natural language. An example could be:

extension Result {
  func whenIsSuccess<A>(
    _ onSuccess: (Success) -> A,
    else onFailure: (Failure) -> A
  ) -> A {
    switch self {
    case let .success(x):
      return onSuccess(x)

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

I'm not sure about it, but at call site it would read much better than fold with the new accepted design, and would recall the previously mentioned when function.

I'm really not seeing the need for further experience with this: there's already plenty of evidence that the accepted design won't work with "symmetric" APIs, and given that some of them (especially constructors) are simply not reworkable, we can easily conclude that the accepted design is too limited. It seems that an optional first label will alleviate the problem, and the response in this thread and in previous discussions seems already more than enough to consider this relatively simple extension.

25 Likes

Hello,
I'm a Swift newbie and just arrived in the forum.

I would like to express the opinion on first closure label/nolabel from a newbie point of view.
When you learn the language closures are complicated/complex without previous knowledge of them.
(I have programmed in Pascal and Delphi years ago, so programming is not completely unknown territory to me)
As I've come to understand closures better, I appreciate their power and usefulness.
But sometimes they can be used in such a manner that it is as if the objective was to make the code as obscure as possible.
*It reminds me of the golden age of C programming when putting a maximum of operators on the same line, making reading and understanding almost impossible. *
Back in those days it was a must for "serious" developers.
Swift wants to be and is a secure and easy to read and learn programming language.
Is it really necessary to introduce elements in the language that makes it more difficult to understand and learn?
I have understood that the multiple trailing closures are a real improvement to the language, but I still have not understood what is gained by leaving the first closure without label.
But I can promise that such an inconsistency will create difficulties for a lot of newcomers.

Happy to join the forum
Best regards
Lars

45 Likes

(emphasis mine)

I think this really speaks to the questions many of us have been asking.

This feature is leading to a substantial change in how Swift officially recommends APIs should be designed and to substantial changes in the surface of the standard library [Update: <= that assertion was wrong, as Ben later made clear], of the sort that—in the past—have been explicitly rejected in the name of stability even when clear improvements were possible with no language changes. For example, I'd have renamed several algorithms in the standard library (some of which were mentioned as justifications for this feature) if I'd had my way. I happen to think that this change is quite clearly bad for the language, but setting that aside, even if it were a little bit good, normally something causing this level of disruption would have to be considered both an overwhelming win and necessary at a very fundamental level.

Hardly anybody seems to believe the change is an overwhelming win, and it's very hard to see what makes it seem to anyone that such a change is necessary. Giving the user more options for writing the same function or method call doesn't enable anything new or important. I just wish I could understand better what the core team thinks the justification is for making a change of this nature, at this point in Swift's evolution. If the criteria for disrupting API and guideline stability have changed, I'd really like to understand what they now are.

61 Likes

My initial hope for Swift, and the Swift Evolution process, was that it would mature and evolve over time in a way that doesn’t force the language into local minimas - which we’d then be stuck in for a decade. I believed that the process would allow the language to progress, and converge, towards an “optimal” form - requiring maintenance of all code bases via mainly auto-migration tools at each major step. I considered this one of its greatest features.

My only criticism of this SE is that I’d rather see the change to the language without restrictions than this.

8 Likes

I too am puzzled by the core team’s decision. I did not participate in the second review thread because I believed that my viewpoint was well-represented by others and it has been emphasized multiple times that proposals are not accepted/rejected based on a majority vote. Looking at this thread now, it seems that it was a mistake - that had a few more of us participated, it might have influenced the final decision (by making it appear that community opinion was converging on an alternative).

WWDC was speculated as a justification for accepting this change. I don’t see how removing a few commas could be this important to Apple. Trailing closures are not mandatory, after all, yet if the reason for this change being accepted is to look good on the slides, it seems that it is being positioned as the preferred syntax. If this isn’t the reason, though, it leaves me even more confused - as @dabrahams pointed out, if such broad changes were possible earlier, we could have ended up with a nicer version of Swift. Why is this change the one officially blessed? It seems like a very minor win, and that only to some.

19 Likes

Edit: removed misguided comment, sincere apologies for that.

Whenever I read things like “let’s see how this goes, real world usage once this ships (and start changing our API’s so good luck asking to revert this later and break everything) and then we can reflect and change based on real experience” I generally, in the workplace, find it as a gentle way to dismiss criticism and proceed with a direction there is little chance and intention of moving away from.

Ultimately it is Apple’s language and they could do like Google did with AOSP and just publish changes in an open source repo after they made the changes they want to make so this process is better than that, aside from when it gets short circuited for some reason...

11 Likes

I think this is a very remarkable thread:
Firstly, it has now 118 posts, which is a quite high number - especially for an announcement(!).
Secondly, nearly all of those reactions are negative; I can't remember a single decision here that got that much backlash.
You may say that this community isn't representative, but that makes the second point even stronger:
The people in this forum are quite enthusiastic about Swift, and those who are unhappy with the decisions of the core team most likely left long ago.

Now, when even the strongest supporters of the current course express their discomfort, I think this should be taken serious; people with less enthusiasm might even be more repelled by SE-0279.

But actually, I think this thread has impact way beyond the addition of small piece of new syntax:
This is also about how Swift evolution works. Yes, it has never been a democracy, but when people get the impression that it's a plain dictatorship, the mood here will change drastically.

20 Likes

I agree in general, but there are a few things that made me posit this:

  • The backlash against last year’s “feature drop” at dub dub was such that they may want to avoid such interpretations.
  • There is a PR spin element to this stuff, and WWDC is a PR event for Apple, as much as a developer conference.
  • Slides and content will have already started development for the conference. Backdating changes may be somewhat painful and have implications for other parts of the company.
  • The final point may well be tied to new API coming through.

I’m not stating this is the case outright. Honestly, it isn’t really relevant. What is, is that the Core Team’s perspective is so clearly out of step with the community on this. That’s what needs to be addressed here. I doubt, however, it will.

2 Likes

I’m in the same case.

I see how it could be.
What I don’t see, though, it how it could be so important to them not to allow optionally labeling the first trailing closure.

1 Like

Just checking. The proposal doesn't solve this problem, right?

There's no way to use a label without putting the closure in parentheses?

let mysteriousSequence = stride(
  from: Int.random(in: 1...10),
  to: .random(in: 100...200),
  by: .random(in: 5...15)
)

public extension Optional {
  /// Wraps a value in an optional, based on a condition.
  /// - Parameters:
  ///   - wrapped: A non-optional value.
  ///   - getIsNil: The condition that will result in `nil`.
  init(
    _ wrapped: Wrapped,
    nilWhen getIsNil: (Wrapped) throws -> Bool
  ) rethrows {
    self = try getIsNil(wrapped) ? nil : wrapped
  }
}

Optional( Array(mysteriousSequence), nilWhen: { $0.count < 15 } )

I like having the following option and want something similar when there are arguments before a trailing closure. Closures in parentheses are gross.

// Label and trailing closure allowed.
mysteriousSequence.drop(while:) { $0 < 50 }
2 Likes