SE-0235 - Add Result to the Standard Library

Even better! Then we can make it easier to bridge between throwing functions and Result.

Very much +1, with Core Team refinements to the exact spelling. I generally agree with @John_McCall's review, and would like to see the spellings refined. However, I also fundamentally believe it is most important that we get the type, and of lesser importance to have an extended discussion on the spellings. My preference would be that the Core Team decide on a finalize spelling, and I would be happy to accept whatever decision they make and update code accordingly.

Unlike some others on the thread, I do not have a strong preference that the spelling match existing conventions, and would prefer the Core Team make a choice based on what is best for the language in the long term. I believe the one time cost to update code to a newer spelling should not be considered a barrier.

Specifics:

  • I don't think fold() is not justified on its own merits, and would recommend its removal.
  • I agree the success and value split should be resolved. I do not think a Value-containing Result always maps to "success", and would prefer the use of value or some (for consistency with Optional).
  • Adding an overload of flatMap in an extension seems risky, and may lead to a poor user experience (diagnostics, in particular). I would need to play around with an implementation to know for sure.
  • I agree with @anandabits that unwrap is better than unwrapped. I agree with @John_McCall that it is unfortunate the name isn't otherwise reflective of the type, but have no concrete better suggestion. I do think that unwrap is fairly obvious, and concise.
  • I agree with @anandabits that the shadowing of Error hasn't seemed to be a problem in practice.
  • I agree with @Joe_Groff that any discussion of deeper compiler integration seems like it can follow later. I think the proposal has enough merits on its own to be included.

I have one cautionary note. My experience with SwiftPM and NIO has led me to believe that it is important to evaluate how the proposed APIs interact with the existing compiler and diagnostics. In particular, it is not always obvious when reading a proposal that the provided overloads will work well, instead of causing a lot of ambiguous diagnostics. I would like to see proposals in this vein be accompanied with a playground which contains an implementation, as well as a large number of examples of common use patterns and common errors, and an evaluation of the behavior.

Definitely. I believe the Result type is essential to writing clean async code in today's Swift, and think the prevalence of it in the ecosystem is supporting evidence for that.

Yes, although as before I think it could potentially be improved further with a few spelling tweaks.

I have significant experience using a Result type in SwiftPM and in other projects that inherit its types. I strongly believe that having a standard Result type will simplify code, especially in places where the only reason SwiftPM is brought in as a dependency is to get its Result type.

A close reading, informed by substantial experience with existing Result types.

8 Likes

In general I think it's a very good idea to strongly avoid overloading, unless there are extremely compelling reasons to overload.

3 Likes

I think it is a little more subtle. Avoid overloads can also have the same problem, where it is very "fiddly" to change something small, and easy to forget, and also results in a poor diagnostic. This is why I think worked examples are key.

1 Like

I'm -1 on this proposal.

The only difference between T -> Result<U, Error> and T throws -> U is the typed Error. If typed errors are the problem, we should extend throws to address just that. Similarly, if (for example) composability of do / catch is worse than function composition (that can be used with Result), we should improve on that. Adding yet another way to return Value or Error is not a good idea. We already have throws and it does just that. As for async, I agree that Result would be useful ... until try / await arrives that is.

TL;DR:

  • sync context — make throws better instead of adding a new solution that does the same
  • async context — avoid temporary solutions, strategically wait for try / await instead
8 Likes
  • What is your evaluation of the proposal?

A hesitant +1 on the implementation details.

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

Yes. It's something that a majority of Swift developers reach for, including myself.

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

I'm not so sure about that. The implementation is close to many 3rd party implementations of Result, including my own. However the type of the failure case is a bit off.

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

Just read the review and a few comments.


I think that this is a good first step and that we could build better language features on top of this type. As mentioned by others, it would feel more natural to allow throwing functions automatically transformed into Result returning functions. However, if we do end up going in that direction, whatever we choose here will effect that quit a bit. Allowing any type for the Error doesn't quit fit that narrative. I can see the reasoning for it when thinking about URLSession, but is that really a pattern we would want to support for synchronous code as well?

What I really want though, is async/await. I was hoping we could skip the language support for Result/Promises etc, but I could see how this fits in, even in an async/await world.

  • What is your evaluation of the proposal?

+1 and long overdue! I've used Result and/or Either in every substantial code base I've worked in.

A few thoughts:

  1. I think map/mapError/flatMap/flatMapError are indispensable and nicely mirror the existing map/flatMap APIs on arrays and optionals.

  2. I think case success/var isSuccess/var value lack naming uniformity/cohesion. (Same with case failure/var isFailure/var error.) Why not name the properties after the case names?

  3. I think the isSuccess/value/isFailure/error properties are all band-aids around the fact that enums aren't as ergonomic as they should be. I pitched a more general solution for this awhile back: Automatically derive properties for enum cases

  4. I find fold extremely useful though I see it mostly as a band-aid around the fact that Swift doesn't have a lightweight expression-based switch statement. I'd probably still define a fold function, though, since it's handy for chaining and passing in functions as arguments, just like forEach.

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

Yes.

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

Yes (other than some of my comments earlier about naming and presence of fields like isSuccess and value).

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

Yes and this proposal compares nicely.

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

In-depth study and experience.

4 Likes

What is your evaluation of the proposal?

+1 provided all the functions are stripped out, they don’t seem consistent with the rest of the standard library, and while I can see them being useful, they should be submitted in subsequent proposals and reviewed separately.

Since it is a contentious issue I also want to note my favor for the naming choice for the cases. A result is logically a success or a failure.

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

Anyone who is active in the wider open source community has experienced problems due to every project having its own Result type and the subsequent need to write conversion functions, and while we could standardize on antitypical/Result, we have not, and I'm not going into why we haven't here as I don't think it is relevant because commonly needed types like Result should be in the stdlib.

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

Mostly. Despite the excellent rationale in the proposal for URLSessions’s closure I still think that having Swift in general be a weakly typed error language has been very pleasant. Most of the time you don’t need to specialize your handler for all error types.

It seems like the URLSession example could be handled by the end user if URLSession had its own error type that provided the URLResponse (if the error occurred after a response was received) and the user could thus use a switch to match for that error type if they cared about it, which most of the time we do not, hence Swift (presumably) picking an untyped error handling system in the first place. For example, this would be the closure contents:

switch result {
case .failure(URLSession.ResponseError(let response, let errorType)):
    // error occurred after a response was received
case .failure(let error):
    // error occurred before a response was received
case .success(let value):
    //

}

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

I have not used a Result type in any other languages (a little in Kotlin, but not enough that I really grokked it in the context of Kotlin).

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

I have at times in the long history of this proposal read in depth. I have also written a Promise library which provides a Result type and used it extensively. I have used a variety of other libraries that provide their own result types. The copious bike shedding over time has put me off getting really into it, but the final proposal is good IMO. I have some caveats as listed above, but overall I think we would benefit from the proposal as a community, so: let’s have it.

3 Likes

I do like the idea of adding Result. It's an elegant way to store results of function calls so you can handle them later. But I do feel the current proposal is trying to do too many things.


I don't feel typed errors are necessary. This example seems to be the justification for accepting typed errors that can accept anything (not even just types conforming to Error):

URLSession.shared.dataTask(with: url) { (data, response, error) in ... }

becoming:

URLSession.shared.dataTask(with: url) { (result: Result<(URLResponse, Data), (Error, URLResponse?)>) in

I actually find the earlier signature easier to understand. The one using Result is more semantically expressive and lets the compiler know about impossible cases, but (Error, URLResponse?) is a muddy way to express an error and exposes impossible cases of its own. If you were to properly declare an enum for the error domain, it'd probably look more like this:

enum DataTaskError: Error {
    case timeout
    case connectionRefused
    case badResponse(URLResponse)
}

Now you know which error cases offer a response object. Additionally the response object is not lost if you decide to propagate the error by throwing. Errors that do not conform to Error are a bad idea.

I don't see any good justification for accepting all types as error, except maybe what's said in the alternatives section about constraining the error type to Error causing problems with capturing thrown errors (is it really true?). To me, the solution is to make the error non-parameterized (Result<T>), so it works well with and similarly to thrown errors.


Speaking of clutter, I don't think most of the proposed methods on Result to be justified. All I'd really need from a Result type is a way to box/unbox thrown errors:

public enum Result<Value> {
  case success(Value)
  case failure(Error)
  public init(_ throwing: () throws -> Value)
  public func get() throws -> Value // unwrapping() in the proposal
}

For instance, there's little need for flatMap when you can express errors in term of throwing:

let newResult = Result {
    try doSomething(result.get())
}

I believe this is the style that should be encouraged because it integrates well with the language's regular error flow. If other errors are thrown within that closure, they'll get wrapped in Result as naturally as it can get.


Is the problem being addressed significant enough to warrant a change to Swift?
Does this proposal fit well with the feel and direction of Swift?

Storing the result of a function and reusing it later happen often enough. Wanting to propagate an error the same way is common too. I disagree about allowing anything as an error in the failure case though. Result should mirror Swift error management, not compete with it by adding more features. And should Swift's error management get more features like typed errors, Result should get them too... somehow.

Also, while the proposal might be motivated by the desire to improve asynchronous functions, I believe asynchronous functions are in need of a better model. I see Result as a stopgap mesure for that use case.

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

I've often used promises in asynchronous contextes, and they look a bit like Result. I tend to prefer when promises work hand in hand with the language's exception/error system.

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

Read the proposal, the review thread, and some parts of the earlier discussions.

4 Likes

Don't map and flatMap seem consistent? And mapError and flatMapError are natural additions since there's an extra generic parameter to handle.

Optional and Array still have flatMap even though you can express the same ideas in terms of if let and for loops. There are plenty of cases where flatMap is more expressive and succinct.

4 Likes

I'm not very strongly against having flatMap on Result. I'd just like to avoid the language promoting two competing error handling system. I see Result as a box you use to store some value/error for later or change context before using/rethrowing, and in that sense I think it fits the language. But the normal flow should be to rethrow errors at the point you're attempting to use the value.

  • What is your evaluation of the proposal?
    +1
    I absolutely love it. Have been using custom versions of this and the antitypical/Result as well. In many situations for modeling something where the Result would not be deprecated by async / await in the future.
    One use case is when using RxSwift with Observables that you du not want to terminate upon failures. By making Observable<Result<T, E>>, you get a structure on which you can filter or map on your ‘error’ situations, and it’s not something that can be replaced by async/await.

  • Is the problem being addressed significant enough to warrant a change to Swift?
    Yes, having a common terminology provided by Swift would be awesome. I care less about the actual naming than just the fact that this will be a standard shared between all Swift developers.

  • Does this proposal fit well with the feel and direction of Swift?
    I think it does, and until we possibly see async/await it would be a great improvement for ObjC APIs to annotate that (T?, Error?) is mutually exclusive and one is required, so it could be imported as a Result in Swift!

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
    I have not.

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
    I have followed the development of the proposal with much interest- and also followed the PR for the implementation from John.

This is excellent work, and I really hope that this will make it into the language!

  • What is your evaluation of the proposal?

An overwhelming +1 from me. We've used Result<T, E: Error> at work for many months now, and it has been fantastic. What's especially nice with having the error be typed is that you can write useful extensions with constraints on that type. This is shown in the proposal sample code. These types of compile-time manipulations aren't afforded to a Result<T>.

Also, If you need untyped error behavior, you can build or the stdlib could provide an AnyError struct that holds an untyped error, OR as I've seen mentioned in this thread, maybe somehow Swift.Error could self-conform, but I'm less familiar with that approach.

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

I believe so, yes. The clarity this can bring to even public Apple APIs where completion handlers are concerned is huge, not to mention that this type can/should have operators like map, flatMap, etc. which people are used to from the standard library already.

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

I think so, and that it is an inevitability that the language gets a first-class citizen Result. It's frustrating when the first thing I must do to bootstrap a new Swift project before starting is to integrate a third-party solution. Public projects are having to do this as well (Alamofire, and SPM itself, for example). For consumers of public libraries doing so, there is the annoyance of having to do specially ordered/specific import statements to avoid ambiguity with referencing the type, when 3 of your dependencies implemented it themselves, and none of those types compose together!

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

The API is very similar to what you'd find with most asynchronous value/stream types like Signal/Future/etc. in third-party libraries, particularly ReactiveSwift.

My experience with Rust (std::Result), Haskell(Exceptional a b), and Elm(Result e v) leaves me feeling like I'm in familiar territory as well. This type has become somewhat of a staple of modern programming IMO.

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
  1. Read the proposal
  2. Looked at the current implementation PR
  3. Have been using/thinking about Result<T, E> for months in a serious, professional environment
  4. Read a good deal of the feedback on this forum page.

Since we can change the language itself there, there is a better solution than AnyError, we can make Swift.Error: Error

3 Likes

Love it! Added that back into my original post as well; really cool idea.

You made a good point here (and a good proposal too, of course). But I keep my argument.

I have many nits to pick with the details of this proposal, but I’ll start by saying I wholeheartedly support adding Result to the standard library.

One of the standard library’s goals is to provide “common currency” types—building block types which are needed frequently in many different domains and systems and often need to be passed across module boundaries. Result is one of those types. It first started to appear before we had native error handling, but it survived throws and it will probably survive async too. We should provide a standard, interoperable implementation for our users.

Now, on to the nits.


The names success, failure, Value, and Error

I think success and failure are great names because they ground Result's cases in specific semantics. The stronger our idea is of what a type is for, the better able we are to provide meaningful operations on it. Naming the cases success and failure helps keep our Result type from devolving into an Either type—and that's a good thing, because Either's cases are meaningless and it makes the type harder to use.

I think Value and Error are the right concepts for the generic parameter names—they should not be something like Success and Failure to match the cases—but
the fact that Error shadows a closely related standard library type is troubling. We should rename that generic parameter to ErrorType, and probably change its counterpart to ValueType to match.

Overall, good work in this area. :+1:

Non-Swift.Error failures

To further ground Result in a specific semantic, I think we should guarantee that the failure case's associated value is a Swift.Error. Ideally, we would take Slava's suggestion and make Swift.Error existentials conform to Swift.Error, but if we can't do that, I think we should drop the second generic parameter entirely and make the associated value an Error existential.

The proposed design bends over backwards to try to support non-Swift.Error errors, locking many useful APIs behind conditional conformances. But the case for allowing non-Error errors is pretty thin. The example Result<(URLResponse, Data), (Error, URLResponse?)> type is a monstrosity that would be difficult for users to handle, would disable a lot of Result's API surface, and could be more gracefully expressed by a custom error type like:

struct URLSessionError: Error {
  var underlying: Error
  var response: URLResponse?
}

If we can promise that Error is always a Swift.Error, I think we can significantly simplify and improve the type, while also further distancing it from Either.

Typed errors

This proposal only touches on the elephant in the room: Do we want to support typed throws?

I think Result should match throws on this question; we want people to be able to move easily between the Result world and the throws world, and if one of them supports more type information than the other, that's going to cause a lot of stumbling.

I think we should decide now between two alternatives:

  1. Result<Value> and no typed throws
  2. Result<Value, Error> and eventual support for throws<Error>.

I don't think we need to rush throws<Error> into the language right away, but I think we should either commit to adding it by Swift 6, or we should drop the Error generic parameter from Result. We shouldn't leave this part of our error design hanging.

I don't have a particularly strong opinion about whether we should or shouldn't support typed errors; my only position is that we should make a firm decision and design our language accordingly.

The unwrapped() method

I think we want a method like this, but I don't like the name because it's the only place where we talk about a Result "wrapping" anything. I would call it something like value() or get() or check().

Note that, if .failure always contained a Swift.Error, this method would always be available. That's another reason to constrain or remove the Error generic parameter.

The value and error properties

r.value is exactly equivalent to try? r.unwrapped(), so I don't think we need it. r.error doesn't have an exact equivalent, but I'm not sure that it carries its weight. If we want it, maybe we should consider supporting a catch? feature instead; then you could write catch? r.unwrapped().

The isSuccess property

We do need a good way to convert a Result to a boolean for simple branching, and I think this is a good answer.

The various map functions

This functionality is central to graceful Result handling. That's why I want it to look very different from what we're considering here.

To start, I think map(_:) and mapError(_:) should take throwing closures. This would basically mean that they dip briefly into the try world and then immediately go back to using Results. For map(_:), there would be no difference if you didn't use throwing expressions; for mapError(_:), you would be able to "fix" a failure by returning a new value, or throw a replacement error. Basically, you get a lot of extra flexibility for almost nothing. They might look something like this:

  public func map<NewValue>(
    _ transform: (Value) throws<Error> -> NewValue
  ) -> Result<NewValue, Error>

  public func mapError<NewError>(
    _ transform: (Error) throws<NewError> -> Value
  ) -> Result<Value, NewError>

(Making this work properly with a generic Error parameter would require typed throws. I'm not sure if that means we would need to defer adding these methods until we have typed throws. Maybe we could use a runtime check for now?)

If we do that, we might then consider dropping the flatMap variants. After all, flatMap { expr } would be exactly equivalent to map { try expr.unwrapped() }.

Finally, we might consider renaming these away from map and mapError to something a little less functional-programming-y. For instance, we could call them continuing (for processing successes) and correcting (for processing failures). We've already changed some flatMap(_:) variants to compactMap(_:); maybe we should take a cue from that.

The Result.init(_: () throws -> Value) initializer

This is a very useful convenience. I considered suggesting it should be an autoclosure, but now that I've written the section on map functions, I actually think using a closure here too creates a nice symmetry.

The fold(onSuccess:onFailure:) method

This is...kind of weird? It's like a general form of all of the other operations, but I'm not sure when you'd use it in practice other than for implementing those. Keep it internal.

Missing conveniences

The biggest thing I wish this Result type included is conveniences for converting from the (Value?, Error?) tuples/parameters we so frequently see today. I can imagine two different ways we might do that:

  public init(value: Value?, error: Error?) {
    switch (value, error) {
    case (_, let error?):
      self = .failure(error)
    case (let value?, nil):
      self = .success(value)
    case (nil, nil):
      self = .failure(Foundation._nilObjCError)
    }
  }

  public static func converting<Return>(
    _ body: @escaping (Result) -> Return
  ) -> (Value?, Error?) -> Return {
    return { value, error in body(Result(value: value, error: error)) }
  }

We might or might not have both, and this might make more sense as part of Foundation.


There's a lot to think about (and argue about) with this proposal, but the bottom line is, I wish we'd had Result in the standard library years ago. I just want to make sure the language and standard library speak with one voice on error typing.

26 Likes

What is your evaluation of the proposal?

Enormous +1.

I like the naming as laid out in the proposal, with the hope that we maybe get more syntax sugar in the future.

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

Yes absolutely, I've used probably 4-5 different implementations of Result in the past couple of years and having a standardized API provided by the language will be a huge boon.

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

Yes absolutely.

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

Nothing in production

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

A quick reading

Could we just stick with #2 and then when default-generic-arguments are supported just introduce that to the type? Can default generic argument be introduced later in an ABI compatible way?

3 Likes

I very much agree with this post, which I think raises a lot of good points. Most of all, I think it's important for error handling to have a cohesive design across the board, so Result shouldn't have a typed error unless we're willing to commit to supporting typed throws.

1 Like