SE-0235 - Add Result to the Standard Library

It's not quite that cut and dry. As soon as you have more than one thing with this sugar, you get composition issues—is () throws async -> () a Result<Future<>> or a Future<Result<>>? More likely what you want when you're inside the context is to have a set of "effects" rather than a nesting thereof. This is a common problem with composing monads in functional languages, and there are complicated libraries built around monad transformers or other formalisms to try to regain composability.

I think it would nonetheless behoove us to integrate Result better into the language; if nothing else, it would be great to extend ?, ?? and other optional-related operators to also work with Result. Like much of the proposed API here, though, it's probably best to consider that additively once we get the basic type in place.

18 Likes
  • What is your evaluation of the proposal?

+1 given the current state of things and the widespread issues/usage of integrating this in projects that need a lot of modularity, slightly more hesitant if the usage will hold up with the longer term direction of async error handling but still see a lot value in having this available

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

We've used the Result enum extensively in projects, and it provides a significant boost to completion handlers. This is generally where I'm hesitant on inclusion: if the largest benefit to this feature is for async error handling, those async errors are significantly better handled with a monadic return like a Future. However, there's still use for this as an additive feature still for semantic reasons, so I have no issue with it in that sense.

Having a shared Result implementation between Swift frameworks without being dependent on common open source libraries (popular ones at that) would be a huge benefit: see the conflicts between Alamofire's Result and a library like Moya which uses a different open source implementation: it's annoying, and this standardization would be a great cleanup of commonly used patterns in the community.

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

This is another point of hesitation for me, as I see it as basically an Either enum if it's not constrained to Swift.Error, with some additional APIs/convenience renaming for the behavior it's used for. The proposal does appropriately address this in the Alternatives section, but I wouldn't be opposed to both if there was a way to typealias Result as a specialized Either.

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

Been following the proposal since the original thread and discussion

Disclaimer: I’m a co-worker of Jon’s and we have been discussing this proposal before its submission.

What is your evaluation of the proposal?

Overall, I am a huge +1, with some caveats:

  • The methods map(_:), mapError(_:), flatMap(_:), flatMapError(_:), and fold(onSuccess:onError:) should probably all take closures that are marked as throws, then themselves be marked as rethrows. I can imagine chaining asynchronous methods together such that the first would retrieve data and the second would attempt to decode the data using a decoder.

  • While we’re talking about those methods, I don’t think they’re necessary at this stage. I’d rather focus on just getting Result in the standard library; these can be bikeshed later. I feel similarly about isSuccess. With the value and error getters, it’s easy enough to use if let to massage the Result as needed.

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

Yes. Many libraries implement their own Result type, and I often implement it myself in apps that don’t use them.

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

With some naming considerations, I think it does. Using Error as a generic name will undoubtedly cause some confusion. Something like ErrorType or Failure would hopefully avoid that. Were I inventing this API from scratch, I might use SuccessType and ErrorType as my generic type names, but then I like the Type suffix.

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

I don’t think I’ve used a language with a Result type in its standard library—or if I did, I never used the type. The closest I’ve come are Objective-C methods that return a BOOL and take an NSError ** parameter.

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

An in-depth review of the code, in-person conversation with Jon, and a thorough reading of the proposal.

2 Likes

This to me feels a bit like saying, “We don’t need hammers. I’ve built a perfectly good nailgun myself.” Sure, it’s technically true, but the Result type allows you to reason about data in a specific way without the extra architecture required by a Promise.

4 Likes

What is your evaluation of the proposal?

-1

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

It depends which problem specifically is being addressed by Result. Essentially all of the compelling cases I see involve asynchronous code. Error propagation in such code is certainly a really big problem, but I don't think this is necessarily the best solution. See below.

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

I don't think so. The idea of using a Result type comes from the pattern of using completion-handler closures. That is, people are (rightly) dissatisfied with this:

performOperation(with: value, completion: { (result: Int?, error: Error?)->Void in
})

and want to write it more like this, to express that the result and error values are mutually exclusive:

performOperation(with: value, completion: { (result: Result<Int, Error>)->Void in
})

So the real question is whether we are happy sticking with the completion-handler pattern as our concurrency model. AFAIK, that model was inherited from Obj-C and its ultimate fate is still unresolved; I have heard plenty of good arguments for using an async/await model instead. So the above code would become:

let result: Int = try await performOperation(with: value)

With no Result type needed at all.

I entirely sympathise with people who are frustrated with the lack of a language-integrated concurrency model in Swift, and have turned to Result<T> as a workaround. Likewise, I know the pain of having to constantly map to/fro between equivalent types from multiple libraries. The truth, though, is that baking this workaround in to the standard library and ABI is entirely premature.

I also consider this is a backdoor way to introduce typed throws in the language, which I'm also against.

Additionally, as @soroush mentioned, I think it's obvious that people will want this type to gain special language support - from if let syntax to implicit conversions between (A) throws -> B and (A)->Result<B, Error>, and maybe even from Result<T> to Optional<T>. I think this is a very slippery slope.

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

I can't directly compare it with anything I've used before, but in general I strongly dislike typed exceptions in languages such as Java. I find that it greatly increases coupling, with little/no benefit; Errors are almost never handled exhaustively.

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

Participated in this and previous discussions, including about typed-throws and the concurrency model in general.

22 Likes

"What is your evaluation of the proposal?"
In my understanding adding Result to the Standard Library is to bring a very narrow solution and point of view, imposing or advocating only one way to solve a problem that can be solved in many other ways.

I also imagine that users and teams create and use over time their own libraries and additional solutions to the Standard Library. I believe that they should be free to implement such thing as Result only if they see it's worth doing and where they think it fits best.

My vote is negative for such inclusion.

"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?"
No.

"If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?"
Good, but it does not fully solve the problem nor give support to the changes that will have to be made in the future; also implementing this proposal now as it is will turn it one more thing to be deprecated sooner or later.

"How much effort did you put into your review? A glance, a quick reading, or an in-depth study?"
In-depth study. I have read carefully the proposal, the cases, the codes, the thread and the arguments for and against; I also compared it with other available and considered solutions; finally, I relied on the knowledge and experience of several programmers as well, myself included.

— Van

That's what they've been doing, and over and over they come up with Result. One of the biggest benefits that this proposal brings is to allow the hundreds of thousands of apps using Result to standardize on one implementation, rather than having a multitude a conflicting implementations.

5 Likes

This is why I'm a -1 on the proposal.

If the language itself is going to have another way to handle errors, it should feel like a first class citizen in the language, and they need to be able to flow well between each other. This proposal does not help with this.

I think Result, in practice, is a better option, but for better or worse, try/throws was chosen, and until we're willing to retrofit that model for something that is all-up better, this feels like a band-aid over the problem.

I also find the examples to be somewhat contrived to illustrate a point. The URLSession example is just a bad API; it should've modeled data better already so that you can't have both and error and data. So I think we're conflating the async nature of the API, which Result doesn't really solve, and the poor API design.

And this pattern:

let one = Result { try String(contentsOfFile: oneFile) }
let two = Result { try String(contentsOfFile: twoFile) }
let three = Result { try String(contentsOfFile: threeFile) }

handleOne(one)
handleTwo(two)
handleThree(three)

Could be just as easily handled with better syntax over try/catch (and yes, a built-in Either or Result type):

let one = try!?! String(contentsOfFile: oneFile)
let two = try!?! String(contentsOfFile: twoFile)
let three = try!?! String(contentsOfFile: threeFile)

handleOne(one)
handleTwo(two)
handleThree(three)

But without the language interaction with try/throws, I don't think it is a good inclusion.

8 Likes

While I sympathise with the problem, I don't consider this a very compelling argument. You could make a single, tiny Result package and try to get buy-in from the various library authors.

The only reason I can see why this isn't sufficient is because of deficiencies in the package manger, and I don't think that's a very good argument to make it part of the standard library. At the end of the day, it's a workaround for the lack of a proper concurrency model.

To put it another way: if we had async/await/Promise/Future, would we still add this, to forever be a part of the standard library? I don't think so.

7 Likes

I mentioned in the "unconstrained-boogaloo" (but wasn't directly responded to on), that I think this:

let one = Result { try String(contentsOfFile: oneFile) }

should instead be: (with compiler support)

let one = catch String(contentsOfFile: oneFile)

catch is already a keyword, in the error handling domain, and this prevents needing to write Result, try, or use a closure.

6 Likes

Yes, if we had async/await, Result would still be a valuable type on its own, for the various reasons laid out in the proposal. It's not proposed as a workaround for anything but as a complement to Swift's current error handling. I expect that a Future or Promise type will play a similar complementing role to whatever form async/await takes in the language, as in both cases it's sometimes necessary and generally very useful to break out of the automatic nature of the language and manually propagate errors or async work.

And it's not as if we're lacking for dependencies (the Result CocoaPod has over 140,000 installs on it's own, not the mention any libraries that bundle their own internal version like Alamofire), but external dependencies only go so far, especially for such a fundamental type. So unless you're arguing that there can never be a migration between community types and the standard library, I don't think "it can just be a dependency" is a good argument once a type has become so popular.

5 Likes

I agree that this is likely to still be useful — if hopefully lower-visibility — after we add language support for concurrency.

4 Likes

While interesting, this form is limited (the closure allows for multiple statements). In the end I view it as strictly additive, as it would be a large proposal and implementation on its own. And unfortunately that sort of language integration doesn't benefit from the extensive use of Result by the community, so we have little prior art to draw on.

This strikes to the point of my resistance to this. Why are people using Result over try/catch? This proposal solves the problem of there being no Result to draw from, but doesn't solve the underlying cause of needing it in the first place.

1 Like

The error manifesto describes several different types of error handling. try/catch is considered automatically propagating. Also described in the manifesto are manually propagating error systems, which is where Result comes in. Just as having to handle everything manually would be a pain, having everything handled automatically, as it is now, can be very limiting. I think that's where a lot of people's pain comes from. Adding Result removes that limitation.

Another point is not just typed throws (which I don't really care about, but which this proposal leaves a door open to), but having a general mechanism to handle failures that aren't represented (or perhaps can't be, due to performance or other reasons) by Error conforming types. So in this way Result serves as a slightly more primitive but flexible error handling mechanism. I wouldn't expect these uses to be very common, but I think that doesn't mean they aren't there.

1 Like

What is your evaluation of the proposal?

Huge +1.

I'm a maintainer for antitypical/Result. I also maintain several libraries that use that Result type. As an OSS maintainer and a Swift user, I think this would be hugely beneficial.

I like the current names. I've found them to be very straightforward and clear when communicating. Having success/failure and Value/Error have the same length adds nice symmetry to the code.

I've also found that being able to talk about values and successes as different things is helpful when communicating. If this were named case value(Value), then value would be ambiguous, which would hamper communication.

I've never had an issue where Result.Error caused any confusion with people or with the compiler as far as I can remember.

There are a few included APIs that I don't think are necessary:

  • fold()

    antitypical/Result calls this analysis. While I have used this on occasion and found it useful, most people I've worked with have found it confusing.

  • unwrapped()

    I've never found myself wanting this. That might be because of my preferred programming style, which is to mostly stay within Result.

I don't object to their inclusion. But if the desire is to start small, I think these could be removed.

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

Yes. There are several popular open source Result types that all compete. I've also worked with a number of open- and closed-source projects that have implemented their own Result out of a desire to have little or no dependencies. That makes interoperability a pain.

While some people clearly don't feel a need for Result or have a desire to use it, there are many of us that do. Anyone who does feels this pain and the community will benefit from a single, authoritative implementation.

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

There's a lot of room for interaction between Result and the rest of Swift, as this thread has highlighted. I think this can fit with the feel and direction of Swift. And I don't think Result is going anywhere—people will continue to use it whether or not it's included in the standard library, so it will be part of the feel and 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?

I've been using and maintaining antitypical/Result since the early days of Swift. I've used it in many open source projects. I've introduced it at work and to many of my colleagues. This proposal is very much in line with that library. I think this covers the necessities of the type.

I haven't spent much time with similar types from other languages, but I've found that my knowledge of Result transfers very easily when reading code or discussing it with people who have used it in other languages.

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

Quick reading of the proposal, skimming of the pitch thread, and years of experience with this API.

5 Likes

I think it's reasonable to see Result and throw as serving different needs within the general realm of error handling and propagation. As I'd mentioned in the previous thread, throw was intentionally designed with an eye toward loosely-coupled, handled-at-a-distance failures, which is why it features automatic propagation (to minimize the overhead of carrying the error to the handler) and no typing (because types would not really help you handle all uncountably many forms of network/file/hardware/etc. error that could accrete through layers of a system). A lot of the people reaching for Result or wishing for typed throws are dealing with more immediate error conditions for which implicit propagation and try/catch ceremony are overkill, and being able to be more specific about the result type is valuable to contain the set of error conditions. It seems to me like it's valuable to have both, and with some investment in language affordances to make Result as fluent to work with as Optional, it could also take pressure off of throws to do more than it currently does.

4 Likes

In my opinion, this is the key part missing from the proposal.

2 Likes

Sure, but alas that's also the part that can be added "at any time", whereas if Result is going to be in the standard library, we'd really like it to be there without ABI versioning constraints.

I also think that the language affordances deserve some longer-term, careful consideration. The affordances we put in place for working with Optional, such as if let and ? chaining, were the best we could come up with given extremely limited time in the pre-1.0 days, but they have clear limitations, and they've gotten in the way of other more general features that could have supplanted them (for instance, pushing pattern-matching conditions into the awkward if case formation). I think it's also true that enum ergonomics in general could use an overhaul; pattern-matching and switching over associated values could be made much easier for all enums, and some of the things you want to do with Result, like bundle up the "error" branches for a chain of Result-producing computations, are things that come up with other enums too.

8 Likes

I think we should care the naming of the similar feature for Future. I personally prefer get like mentioned in Concrete proposal for async semantics in Swift · GitHub . Signatures in the following code have consistency.

extension Result {
    func get() throws -> Value { ... }
}

extension Future {
    func get() async -> Value { ... }
}
2 Likes