[Revised] SE-0235: Add Result to the Standard Library

SE-0235: Add Result to the Standard Library has been revised and is now open for review through Sunday, December 2nd. Based on the first round of feedback, the core team agrees that this feature is worth adding to Swift, and we are now looking for feedback on our proposed revisions to the proposal, which are at times substantial. As Chris Lattner is currently on vacation, I will be taking over as review manager.

Thank you for everyone who participated in the first phase of review. Feedback there was generally positive on the idea of standardizing the widely used and reimplemented type. The core team discussed several important points raised in the discussion:

  • Several people expressed hope that a future async/await model would obsolete the most important use-case for Result. The core team agrees that async/await is likely to greatly diminish the importance of this type, but not so much as to make it completely useless, and the type will help standardize existing practice in ways that are likely to be useful for async/await.

  • Several people suggested that adding optional chaining and other syntactic affordances to the proposal would make it stronger and more useful. The core team feels that the type is quite useful even without those affordances and that they can considered in a future proposal.

  • Some people suggested that Result could be renamed to Either to generalize it. An Either type has been discussed many times in the past, and while that may be independently useful, the core team felt that an error-handling biased type would be a better path forward for expressivity and modeling power. For example, while it might make sense for optional chaining to apply to a Result type, it certainly wouldn't make sense for it to apply to a general Either type. Result types are also widely used in practice, so there is a great deal of precedent for their utility.

As for the details of the proposal, the core team agrees about many points:

  • The core team agrees with the naming and semantics of the proposed map, flatMap, mapError, flatMapError, and unwrapped methods.

  • The core team agrees with the community sentiment that shadowing Swift.Error type with the Error generic parameter is not problematic in practice.

  • The core team agrees that Result should conditionally conform to Hashable and Equatable .

The core team has spent significant time integrating feedback from the community and has come up with a list of refinements to narrow and tune the proposal:

  • The Error type parameter should be constrained to conform to Swift.Error. This reinforces the intended purpose of this type and makes it easier to use it together with Swift's ordinary error-handling. It also gently promotes better error-handling practices, such as using meaningful enums and wrapper types rather than raw integer codes and Strings. This will not interfere with the potential future addition of a "typed throws" feature because error types under such a feature will certainly still be required to conform to Swift.Error.

  • In combination with the above, the Swift.Error protocol type should be made to conform to the Swift.Error protocol. This is an instance of a general language feature called "self-conformance", which is currently restricted so that it only applies to certain @objc protocols. The core team investigated and decided that lifting this restriction specifically for Swift.Error is straightforward. This change permits Swift.Error to be used as the Error type even if Error is constrained to Swift.Error, as above; it also simplifies certain other efforts to write code that is generic over error types.

  • The case names of the enum should be renamed to align with the generic types, specifically using these names:

    public enum Result<Value, Error: Swift.Error> {
        case value(Value)
        case error(Error)
    }
    
  • The value(), error(), and isSuccess() methods should be removed. They are useful sugar, but can and probably should be handled by some more general language feature that applies to all enums in Swift automatically.

  • The fold method should be removed. The core team agrees that there is utility to include something like this function, but it is best to take time to determine the best name for this, and it can be added as a separate additive proposal later. Furthermore, fold is essentially a switch expression; it may be better to simply add a general language feature for that than to add a special version of it for Result.

  • The throw-capturing initializer should be renamed to init(catching body: () throws -> Value), giving it an argument label if it used with a named function value instead of a closure. Unlabeled initializers are conventionally conversions, and this is not a conversion of the function value.

  • The throwing overload of flatMap should be removed. It is composable from flatMap and init(catching:) , and it is generally frowned upon to overload just on whether something is throwing or not.

  • Result should not conform to CustomDebugStringConvertible; the default behavior should be reasonable.

Thank you to everyone who participated in the first phase of this review. In this second phase, the core team has accepted the value of this feature in the abstract and is specifically looking for feedback on the proposal as revised above. All review feedback should be either on this forum or, if you'd like to keep your feedback private, directly to me (not Chris) as the review manager, either using a direct message on these forums or by simply sending me email.

(Large portions of this post were written by Chris Lattner, but any faults in it are my own.)

42 Likes

I strongly agree with all of the decisions made by the core team (with one small and relatively unimportant nit - I think unwrap would be a better name than unwrapped).

6 Likes

Is it possible/feasible for these to be part of the initial implementation, and the removed/deprecated in favor of the compiler-provided ones, when that is added at a future time?

2 Likes

Not easily, since the names would now collide.

2 Likes

Agreed -- I'm really happy to see all of these revisions! I also think unwrap would work better as a function name, but unwrapped would be better if it was a computed property instead. It's a minor detail though as you said.

2 Likes

One thought on the case names: In my own projects, I sometimes use a Result with a value type of () to represent an operation that can succeed without returning any specific data. In that scenario, it feels like success / failure would make more sense than value / error, since there is no value being wrapped per se. Not sure if that's a common use case, but thought it would be worth bringing up.

12 Likes

I agree with many of these decisions. In particular, unwrapped conforms to the "ed/ing" rule in a way that unwrap does not, so I cannot see how it could be named otherwise.

I would quibble with one specific decision and hope to add one decision to this revision on the basis of what's already being revised:

  • I think the original case success(Value), failure(Error) would be superior to the proposed revision, and not merely because of familiarity. First, success conveys the semantics of the case better than value, and when read together as "success value" is vastly more meaningful than "value value." Second, it parallels the naming found in Optional, where the cases are not named in a way that duplicates the associated value type alias.

  • Because I very much agree with the conformance of Error to Swift.Error and self-conformance of Swift.Error, I think it would be excellent and trivially difficult to conform Never to Swift.Error: it has often been said that Result<T, Never> carries meaning, and it would be nice that it be expressible in Swift out-of-the-box, for didactic purposes if nothing else (though I suspect it could be useful in generic contexts). In the future, the model would be made complete when compiler smarts are added so that we have the right subtyping relationship between T and Result<T, U> and Never becomes a true bottom type.

35 Likes

+1 on the Never: Error bit. That's a great observation.

5 Likes

I had the same objection originally, and was going to say that by convention we should use unwrap() for a method name and unwrapped for a computed property name.

But when I double checked String, with the intention of citing the String.lowercased property as an example to support my objection, I realized that String.lowercased() is actually a method and I was completely wrong about the convention.

The actual convention in Swift (insofar as there is one) is that you’d use “unwrap()” to unwrap a mutable value in-place and “unwrapped()” to indicate that the method returned an unwrapped value without mutating the original (as is the case here).

An example of this from the stdlib would be Array.sort() vs Array.sorted().

6 Likes

While I'm happy to see the proposal continue to advance, and I agree with most of the changes for future-proofing, I have to agree with this argument for the case naming here. In fact, now that Error will be constrained, I think success/failure make even more sense than they did in the unconstrained version.

I'm happy with everything else and will work to rebase my implementation PR on #20629 this weekend.

3 Likes

This was already done in SE-0215. (PR here)

14 Likes

Strong +1 from me, and I agree with most of the changes, but also share the view that success/failure better describe the cases.

I can see the thinking behind having the case named value and error for consistency, but in my view the case and the type are describing different things:

Success is the status of the Result. The Result was success, and the fruit of that success was a value. The value is not the status, especially (as others have pointed out) since that value may be of type Void in situations where we only care about the status.

I don’t feel very strongly about this, and I’d much rather have Result with these names than not at all, but since naming cases to match the values is not an established convention in Swift already (e.g some(Wrapped) for Optional), I see no reason to deviate from the de-facto standard of success/failure.

22 Likes

This is great. :heart:

It's a relief to have the core team weigh in on these issues—especially whether Result should be included, whether it should be constrained to Swift.Error, whether Swift.Error should be self-conforming, and that some of the ergonomics of enums that could be improved. I'm very happy with this direction.

I also feel that success and failure are better names for the cases, as I said in the earlier thread. @nicklockwood explained it well.

4 Likes

I primarily agree with this version of the proposal, as updated by the core team.
However, I still think its case name should be some(_: Value), which I personally considered independent to my suggestion of using "Wrapped" for the name of the Value generic parameter.
I now however wholly agree that "Wrapped" is an inappropriate name for the Value generic parameter of Result.

Speaking just for myself, I don’t consider Optional’s case names to have precedential value; not only do you rarely have to use them, but they’re also more of a grandfathered exception to the naming rules than something that’s particularly in keeping with them.

3 Likes

Thanks for the explanation of the unwrapped name. I guess I haven't fully internalized this convention yet. This makes sense so I retract my previous nit.

In light of the other comments about success / failure, I would like to reiterate my comment from the previous review thread that I personally find value and error to be much better names and am very happy to see that the core team has made this change to the proposal.

1 Like

I'm personally partial to ok / error like what Rust does, but I recognize the semantic value of value / error.

This version is much better than the first one. I happen to prefer value and error over the alternatives, as well.

I have not examined every previous megabyte of discussion on Result, but one thing I have not found is a justification for effectively making switch co-equal with do-try-catch for error handling. I personally don't like that at all, and it's a huge departure for Swift error handling. Is it desirable?

All the other advantages of a Result type can be had with a version that isn't (outwardly) an enum, so that extracting values from it must be done using unwrapped() or the optional accessors. I know that catch falls down a little bit on account of not having typed throws yet, but I'm assuming that's temporary. (there would be a clean workaround, since a non-enum Result would have the optional accessors that have disappeared from this version.)

1 Like

Such analysis is given at length in both the proposal itself as well as the linked review thread and previous discussion threads.

The proposal only talks about the advantages for asynchrony and delayed handling -- and they are obvious. There is, however, nothing about why we need to make switch co-equal to do-try-catch. Result doesn't have to be a straight enum to provide all its advantages for asynchrony and delayed handling. But, if it is an enum, then switch will take the place of do-try-catch in many code bases. Again, is this desirable?

I promise I scanned the discussions at many points, but have not found why Result must be an enum instead of a struct. I don't think that "because it was the obvious approach" is necessarily the convincing answer.

1 Like