SE-0235 - Add Result to the Standard Library

What is your evaluation of the proposal?

(Almost) :100:.

I would prefer result.success instead of result.value since some code often becomes result.value.value .

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

Quick reading proposal, and (previously) in-depth study with following articles explaining the importance of generic error (especially when using NoError to reduce overloading ):

2 Likes

I see what you mean. On the conclusion, however, I differ: I do call for an opinionated Result type, with an expected benefit of standardization across various code bases.

Yes It can be Swift's responsibility to drive the community. Current error system does. Optionals do. Codable does. POP does. Pointers do. Those are all very very opinionated tools. Apologies for the ones I forgot: readers will insert their own favorite Swifty pattern here.

It is possible to be both opinionated and versatile.

To reach this goal, one must dive in the kind of code that is 1. desired and 2. expected after the proposal has been accepted. One must make an opinion on the desired code. Make it clear. One must also explore various needs, see if they fit. Show how well designed the proposed type is. One must also explore various misuses, see if they are made difficult enough. Address eventual shortcomings with documentation recommendations, compiler warnings, etc. One must finally explore various unexpected uses, and see if they are still consistent.

To give a single example of unexpected use, where are the nested Results in the proposal? The comparison of Result with Optional is sound - but since we have double optionals, should we also expect double Results? Or not? If no, how do we prevent them? Did we check that the various map and flatMap overloads can't create unexpected values? And is it a correct timing, here in the review phase, to do this exploration?

Result is such an important type! Will we have enough QA minds?

5 Likes

When I came into this thread I thought I would be a big +1 on this, but since reading through the discussion I'm now a -1.

I use Result all over the place, it's incredibly useful, but it's only useful because it patches a bigger problem with concurrency in Swift.

I've just read through the async/await proposal by @Chris_Lattner3 and it just makes much more sense to fix the problem at a language level rather than at a type level. I think it would be much more useful for everyone to take a step back and look at why Result is being proposed in the first place and then understand if the underlying issues as to why it exists can be fixed in a better way.

I think we should take the Apple approach here. Let's not just rush something out to patch over a problem in an "it'll do" kind of way, let's look at the problem as a whole and see if there is a better way to approach it and make it so Result doesn't even need to exist in the first place. I would rather it took longer to fix the core issue than to throw out a patch and regret it later.

26 Likes

A Result type is very useful, and in principle I support adding it. I commend @Jon_Shier for moving this proposal forward.

However, as several people have commented, what we lack at the moment is a clear public vision for how Swift is expected to evolve post Swift 5. This is very understandable given the current time constraints.

Without this, it's difficult to evaluate how this proposal would fit into and interoperate with any possible future features such as promises/futures, typed throws, async/await, language support/sugar for Result etc.

Hence I think I'm +0 in the absence of this.

2 Likes

I prefer Result<T> and used that for a long time.

enum Result<T> {
    case success(T)
    case failure(Error)
}

extension Result {
    
    func resolve() throws -> T {
        switch self {
        case .success(let value):
            return value
        case .failure(let error):
            throw error
        }
    }
    
    init(_ expr: () throws -> T) {
        do {
            let value = try expr()
            self = Result.success(value)
        } catch {
            self = Result.failure(error)
        }
    }
    
}

Just use generic Error: Swift.Error. Specify the error type is limit the Result function. Catch and handle the default error case is duty of us.

(A framework throws system domain or custom error type is normal. Sometimes API could also return a unusual error type.)

The most important is throws is not specify the error type. We could .flatMap it and throw a new error. So we can pass it between multiple logical layers.

IMHO. It's not difficult to create and reference the definition of Result by hand. If everyone has different tastes, don't introduce it into the standard library is best. XD

e.g.
https://github.com/Alamofire/Alamofire/blob/master/Source/Result.swift
https://github.com/antitypical/Result



3 Likes

I want Result<T> instead. Having to type Result<T, Swift.Error> everywhere would quickly become annoying and tedious, and I have no use for setting the Error type to anything else.

4 Likes

A definite +1 from me.

I think Result is useful, and agree with the motivation. In my code, it's been most useful when I wanted typed errors, and when dealing with asynchronous code. I also like the fold extension. For me, Result.fold (just like Result itself) is one of these things that might seem unnecessary until you've used it, but becomes really useful once you get used to it.

I also think it's a bit unfortunate that Swift will have two different mechanisms if this is accepted, but I do think the benefits of Result are easily worth it. Either we can address this in the future, or maybe the two alternatives happily co-exist.

7 Likes

I was wondering if it will be more acceptable to put the Result type in corelibs Foundation instead of stdlib.

4 Likes
  • What is your evaluation of the proposal?
    -1

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

  • Does this proposal fit well with the feel and direction of Swift?
    Unsure with respect to concurrency, hence I'd prefer that this be postponed until after there's more clarity on that, or alternatively to be reviewed in conjunction with.

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
    Considering this was never prioritised upfront; I think everyone that needed this, already has it covered. As for other languages, I have a preference for a more complete Either / Validation type.

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
    Followed along for the most part with the formative discussion thread, and did a quick review of responses here.

  • What is your evaluation of the proposal?

I think it should be added.

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

Yes. Especially with async APIs. But not only.

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

Yes. It is much nicer and way to use for instance URLSession where closure has 4 optional parameters.

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

I have used https://github.com/antitypical/Result Author of that library already commented in this thread.

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

I would really appreciate the official addition of the Result type in the stdlib. You can also count me into the camp of people who want the typed throw mechanics in Swift. The overall proposal is a great start and almost as minimal as it should be. However I raised a few concerns regarding some of the proposed API's in the pitch thread, but those things didn't get into the alternative suggestion section of the proposal. So for those of you who did not follow the latest pitch thread I'd like to point out those things again:

  • I would like Result to have a symmetric set of transformation methods:

    • mapValue and mapError
    • flatMapValue and flatMapError

    map and flatMap can be reserved for a more generalized meaning. For instance with HKT map seems very similar to the currently proposed fold method.
    As proposed, in every case we are mapping one result into an other result, and we're always mapping one captured value no matter if it's of Value or Error type. A general map or flatMap should map the captured value but not just one specific type of it. Therefore I don't think that the chosen naming scheme reflects the intended semantics.

  • We have to consider the inclusion of the unwrapped method very carefully to potentially avoid future source breakage or incompatibilities.
    In this thread it was mentioned that we could make an exception for the Error protocol to conform to itself to fix one of the major limitations involved in the error handling mechanics. Aside that pitch and without the knowledge if upgrading an existing throwing method to a typed throwing method is a breaking change or not I'd say that the proposed unwrapped method in the extension with the Error : Swift.Error constraint should be renamed since it's explicitly loosing the Error type.

    extension Result where Error : Swift.Error {
      // Not currently proposed, because this is not possible to express
      // in the current Swift.
      // This creates perfect symmetry with the extension where the
      // constraint is `Error == Swift.Error`.
      public init(_ throwing: () throws(Error) -> Value)
      public func unwrapped() throws(Error) -> Value
    
      //================================================================
      // Renamed because the type error type is lost: strawman syntax
      // The type should be equivalent to `() throws(Swift.Error) -> Value`
      public func relaxedAndUnwrapped() throws -> Value {
        return try self
          .mapError { $0 as Swift.Error }
          .unwrapped()
      }
    }
    

Furthermore I haven't seen anyone mentioning that Result in conjunction with with the current error handling mechanics can enable new things like chainable monadic value / error transformations or type alignments, which would be still very inconvenient to achieve without Result even with the bare typed throws and a new co-routine async / await model.


That said I totally think that both mechanics can safely co-exits and used in a nice mixture. Both Result and even typed throws can exist together without creating an issue for one another. If you don't want typed throws, don't use them, but don't prevent others who need them to use them. If you don't want typed error on Result just create a simple type alias like SimpleResult<T> = Result<T, Swift.Error> and use that everywhere. I think the long term goal is that transformations from the first class error handling mechanism into Result and vice versa should be made seamless, which will grant a lot of convenient flexibility and also satisfy everyones needs.

1 Like

I agree that async/await looks promising, but adding Result is orthogonal to the async/await work. It doesn't delay or otherwise hamper that effort especially since there is currently no plan to add language sugar to support it (which is the only way in which it might potentially conflict with async).

Result is a simple solution to a couple of real problems that we currently have to deal with (exposing async errors and typed errors in APIs). The fact that it may be partially obsoleted by a much more complex feature in the distant future is not a good reason not to add it.

6 Likes

I'm a big +1 on this proposal.

I think people who see Result as trying to tackle some kind of concurrency primitive should take a step back and look at what Result is really meant for: to communicate the status of a function. I think it's perfectly acceptable to treat Result as its own useful type that can function along-side of other mechanisms in Swift.

I also don't see the argument that because this design isn't "convenient" enough to use, it should be put back into pitch phase. Just because something isn't as good as it could be someday doesn't mean we should withhold adding it now. If the SIMD proposal is any precedent, there are many convenient features that have been purposefully stripped out of it to make it a clear cut proposal. Which I think is fine.

Honestly the thing that has me the most worried, and I haven't really seen discussed in-depth. Is if a migration to move collisions is really something that is going to be feasible for large projects. Especially ones that aren’t rolling their own type internally.

2 Likes
  • What is your evaluation of the proposal?

+1

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

Yes. In the same way that a Tuple is a fundamental data structure in a language with algebraic types and a type system strong enough to back up words with action, so too is (what here is called) Result. Empirical evidence of this is borne out in the hundreds of Swift libraries that inevitably write some form of this into a support library, or link with the many packages and frameworks that have a Result-like among their APIs.

Allusions to exception handling, capturing types for asynchronous code, and the desire for syntactic sugar are just further evidence of its sheer ubiquity and flexibility as a type. Even if you haven’t used it in your code, you more than likely rely on some framework that does.

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

Yes.

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

Result plays a prominent role in Rust, enough that they have multiple language constructs, macros, and syntactic sugar to help push Result as the canonical type of error-returning code.

Haskell and its kin define Either as a data structure, taking a right-biased stance for a number of reasons that aren’t important to this discussion.

Cats in Scala defines Either neutrally, choosing instead to provide left and right projections as views that define partial operations for map, flatMap, etc.

This Result behaves most closely to Haskell’s Either, which I think strikes the right balance here. We don’t have a macro system, so providing sugar means an actual addition to the language that should go through a separate review. Cats’ approach, while conceptually clean, doesn’t capture the 99% case where Result communicates a computation can possibly fail or succeed as effectively as biasing does.

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

I started the (first?) discussion on including a disjoint sum in the standard library.

8 Likes

I have mixed feel of this proposal.

May be we need Either<T, U> for migrate two-alternatives complex code (usually implemented through arguments list, several properties and optionals) to more simple, straightforward and robust one. Yes, the most popular case is Result semantic in asynchronous/networking code but 1) it's just a special case of Either<T, U> (although again the most popular) 2) it is hard to select universal namings (for example, not always «success/failure» is really correct namings in codebase context).

Also may be after asyc/await or similar feature implementation it will cease to be relevant? May be we can defer this feature to planned Swift asynchronous syntax improvements and then we could analyze how the «Asynchronous Swift» has changed in every-day code. One day ObjC blocks already changes our asynchonous code dramatically. If Result will be still relevant then it will be ok. In any case everyone can add it’s own Result with preferred namings in any moment now and it is not «blocker».

Also one important note. How Result will be incorporated to ObjC? Most of Cocoa is ObjC and if Cocoa asynchronous code will not change then it loses a lot of motivation base. Lets see to macOS XPC inter-process communication. We are strongly tied here to ObjC blocks internals and arguments order for asynchronous inter-process «messages». Yes, we need improvements here for error processing but how will Result help us in this case? Yes, we can drop this and focus only on pure Swift cases but then it will not be major improvement for every-day iOS/macOS development.

Result concept is useful in some cases and I'm not in opposition to this priposal but I think we need more fundamental and comprehensive solution.

Thats true, it doesn't hamper that effort, but also we aren't hampered by not having it. Everyone has their own solution right now that works for them. I don't think rushing in to add something that isn't necessary is the right thing to do.

The problem with the current proposal is that it constrains the error types which in itself is trying to support a future feature before its ready without an alternative. We shouldn't be forced to specify the error type when no part of the rest of the language is forcing us to.

The inescapable fact of this is that it will effect the ABI by adding it to the stdlib. If we already know that it's going to be obsolete at some point do we really want to add that cruft right now when we already have a solution to the problem?

(I just want to reiterate that I like Result, I think it solves a problem we have right now, but I don't believe it's worth adding it to the stdlib when you look at the pros and cons on balance.)

4 Likes

AnyError is mainly a workaround for Result types that use the : Swift.Error constraint which this proposal does not. However, @Slava_Pestov mentioned making Swift.Error conform to itself. This is a much better solution than AnyError. I encourage the core team to include this modification if the proposal is accepted.

6 Likes

Thats true, it doesn't hamper that effort, but also we aren't hampered by not having it.

The async/await effort is not hampered by the lack of Result, but other efforts are, outside of the Swift stdlib itself - such as exposing typed or async errors at the interfaces of 3rd party Swift libraries. Right now we can (and do) define our own Result types, but they can only be used internally within an app or library - they can't be shared between libraries without adding a common dependency.

The problem with the current proposal is that it constrains the error types which in itself is trying to support a future feature before its ready without an alternative. We shouldn't be forced to specify the error type when no part of the rest of the language is forcing us to.

As I understand it, that's not true. Because the base type of Result.Error is Any, you can use Swift.Error as the error type if you don't want to be more specific.

But the fact that you can be more specific if you choose to is one of the problems that Result is specifically trying to solve, because there's currently no other way to do strongly-typed error handling in Swift, and proposals for adding typed throws have been stuck in limbo.

1 Like

+1

After several months of struggling on determining a proper error handling mechanism, I finally found the Result is the best for now. It is quite useful on async errors in a callback, providing a good syntax and mental model. I am now migrating all my things to a Result style API. I found it unfortunate that I have to depend on or write the Result type again and again for my projects. I would be very happy if this kind of basic thing could be provided in std lib (just as Optional).

The strongly typed error gives users confidence in handling the error in a correct way and reasonable coverage. Compared to existing try/throw (and also the notorious Error?), it encourages users to take good care of error case and write better error handling code. So it will indeed improve the language and it is a good direction. Basically, throws and Result are solving different problems, one for a long path error propagating, another is for a short-term callback with a typed error. It is not strange that they existing and complementary each other.

There's some suggestion on Either instead of Result. I am a fan of FP myself, but in my daily life, I believe I use the specialized Result version for more than 99% cases. It describes the domain more clearly.

1 Like

I've not had much of an issue in this area, maybe I've been lucky, but I don't think this is as big of an issue that people can't get around it.

Sorry I was mistaken, what you say is correct. But it's also correct in saying that if you are forced to use Swift.Error everywhere it's almost forcing you to be more specific when in a lot of cases you don't need to be.

I used to think that I wanted strongly typed error handling, but I've recently changed my mind on that subject. I will keep my opinions to myself on that subject though as it caused some heated debates in a different thread :grimacing:

1 Like