SE-0235 - Add Result to the Standard Library

I haven’t done specific testing, but I would expect that the fixed costs of instantiating metadata far outweigh any per-generic-parameter cost. Once metadata is instantiated, it’s cached, and looking up a two-pointer key in a hash table isn’t all that much more expensive than a single-pointer key. The runtime has specialized code paths for 1...4 generic parameter metadata caches as well.

The throwing signature for the transform is effectively just different syntax for a transform that returns Result<NewValue, Error>. That is why this method has flatMap semantics. compactMap is a filtering map. I don't see any filtering happening here. :wink:

I find your proposed map and flatMap signatures are troublesome. As I have said many times before, I don't think we should have multiple error pathways in the same operation. If it returns Result it should not throw.

1 Like

-1.

I think adding a Result type to the standard library could be nice, but in it's proposed form I'm going to have to vote no. The proposed Result<Value, Error> is marked departure from the current error-handling system which uses the Error protocol as first-class citizen. I think it would be a huge mistake to bifurcate Swift with two orthogonal error handling mechanisms.

I think the following amendments would make much more sense and be much easier for Swift developers to use today:

/// A value that represents either a success or failure, capturing associated
/// values in both cases.
@_frozen
- public enum Result<Value, Error> {
+ public enum Result<Value> {
  /// A success, storing a `Value`.
  case success(Value)
  
  /// A failure, storing an `Error`.
  case failure(Error)
  
  /// The stored value of a successful `Result`. `nil` if the `Result` was a
  /// failure.
  public var value: Value? { get }
  
  /// The stored value of a failure `Result`. `nil` if the `Result` was a
  /// success.
  public var error: Error? { get }
  
  /// A Boolean value indicating whether the `Result` as a success.
  public var isSuccess: Bool { get }
  
  /// Evaluates the given transform closure when this `Result` instance is
  /// `.success`, passing the value as a parameter.
  ///
  /// Use the `map` method with a closure that returns a non-`Result` value.
  ///
  /// - Parameter transform: A closure that takes the successful value of the
  ///   instance.
  /// - Returns: A new `Result` instance with the result of the transform, if
  ///   it was applied.
  public func map<NewValue>(
-    _ transform: (Value) -> NewValue
-  ) -> Result<NewValue, Error>
// closures that throw should be automatically create new results with thrown errors
+    _ transform: (Value) throws -> NewValue
+  ) -> Result<NewValue>
  
  /// Evaluates the given transform closure when this `Result` instance is
  /// `.failure`, passing the error as a parameter.
  ///
  /// Use the `mapError` method with a closure that returns a non-`Result`
  /// value.
  ///
  /// - Parameter transform: A closure that takes the failure value of the
  ///   instance.
  /// - Returns: A new `Result` instance with the result of the transform, if
  ///   it was applied.
-  public func mapError<NewError>(
-    _ transform: (Error) -> NewError
-  ) -> Result<Value, NewError>
// This would be vastly simplified, simply take an error and return a new error if you so choose
+  public func mapError(
+    _ transform: (Error) throws -> Error
+  ) -> Result<Value>
  
  /// Evaluates the given transform closure when this `Result` instance is
  /// `.success`, passing the value as a parameter and flattening the result.
  ///
  /// - Parameter transform: A closure that takes the successful value of the
  ///   instance.
  /// - Returns: A new `Result` instance, either from the transform or from
  ///   the previous error value.
  public func flatMap<NewValue>(
-   _ transform: (Value) -> Result<NewValue, Error>
-  ) -> Result<NewValue, Error>
// Again throwing would be permitted integrating nicely with our existing system
+   _ transform: (Value) throws -> Result<NewValue>
+  ) -> Result<NewValue>
  
  /// Evaluates the given transform closure when this `Result` instance is
  /// `.failure`, passing the error as a parameter and flattening the result.
  ///
  /// - Parameter transform: A closure that takes the error value of the
  ///   instance.
  /// - Returns: A new `Result` instance, either from the transform or from
  ///   the previous success value.
-  public func flatMapError<NewError>(
-    _ transform: (Error) -> Result<Value, NewError>
-  ) -> Result<Value, NewError>
// Again a vastly simplified signature with handling of existing niceties
+  public func flatMapError(
+    _ transform: (Error) throws -> Result<Value>
+  ) -> Result<Value>
  
  /// Evaluates the given transform closures to create a single output value.
  ///
  /// - Parameters:
  ///   - onSuccess: A closure that transforms the success value.
  ///   - onFailure: A closure that transforms the error value.
  /// - Returns: A single `Output` value.
  public func fold<Output>(
    onSuccess: (Value) -> Output,
    onFailure: (Error) -> Output
  ) -> Output
}

- extension Result where Error: Swift.Error {
// Simplifies these extensions
+ extension Result {
  /// Unwraps the `Result` into a throwing expression.
  ///
  /// - Returns: The success value, if the instance is a success.
  /// - Throws:  The error value, if the instance is a failure.
  public func unwrapped() throws -> Value
}

- extension Result where Error == Swift.Error {
+ extension Result {
  /// Create an instance by capturing the output of a throwing closure.
  ///
  /// - Parameter throwing: A throwing closure to evaluate.
  @_transparent
  public init(_ throwing: () throws -> Value)
  
 -  /// Unwraps the `Result` into a throwing expression.
 -  ///
 -  /// - Returns: The success value, if the instance is a success.
 -  /// - Throws:  The error value, if the instance is a failure.
 -  public func unwrapped() throws -> Value
  
-  /// Evaluates the given transform closure when this `Result` instance is
-  /// `.success`, passing the value as a parameter and flattening the result.
-  ///
-  /// - Parameter transform: A closure that takes the successful value of the
-  ///   instance.
-  /// - Returns: A new `Result` instance, either from the transform or from
-  ///   the previous error value.
-  public func flatMap<NewValue>(
-    _ transform: (Value) throws -> NewValue
-  ) -> Result<NewValue, Error>
}

extension Result : Equatable where Value : Equatable, Error : Equatable { }

extension Result : Hashable where Value : Hashable, Error : Hashable { }

extension Result : CustomDebugStringConvertible { }

What changes would need to be made to Swift to have an Error generic parameter make sense? There are a few things that could help:

  1. Default generic types
    If you can declare Result<Value, Error = Swift.Error> that would dramatically reduce the boilerplate for what will be, by far, the most common use case.

  2. Add typed throws
    Now a Result specific Error type makes total sense. Without typed throws it would be a strong divergence from error handling in modern Swift.

  3. Add union types
    The biggest pain in practice with typed errors would be how unwieldy type composition is. To compose errors that bubble up would require defining enums and wrapper types everywhere. If you could simply declare that a function throws MyError | BespokeLibraryError then the usability cost of typed errors is dramatically reduced.
    EDIT: Which I should note will not happen, proposals for union types have been rejected. This is a reason to be skeptical that typed throws will ever be adopted.

Let me emphasize that I love types and I love Result, which I've used in my own work. But defining a Result specific Error type doesn't make sense for Swift, not without some significant changes to the language. That's why I'm against this proposal.

4 Likes

In makes only sense to me if you treat the closure as another Result, but the flattening aspect of that is hidden and not obvious. I think that particular methods tried to merge the init and flatMap which is too much. I would drop that last flatMap.

1 Like

There is another interesting performance consideration for unboxed Result types—they're not ideal for code size when propagated through many frames. Because unboxed Result<T, U> has a different size and layout for every T and U, propagating an error may require reconstructing a new Result into a different-sized indirect return buffer, or moving back and forth between register and in-memory returns. This is a problem in Rust, where Result is the idiomatic error propagation type; even simple error-propagating code spends a number of instructions moving pieces of Results from one buffer to the next:

Swift's throws avoids these issues by using a uniform boxed representation for Error, and a special calling convention that makes testing and propagating error results cheap; a Swift function can propagate an error by returning while leaving the existing error value in the error register (r12 on x86-64):

If we anticipate that people will want to use Result as a propagation mechanism, we many also want to consider whether it should be an indirect enum, which would give it a uniform representation that can be propagated more inexpensively. This would also give it a fixed layout so that it can be worked with uniformly in unspecialized generic code. The downside would be that constructing a Result would require boxing and allocating.

16 Likes

I can't speak to the rest, but I don't think we will see Result propagated through many frames of a synchronous call stack. Certainly not if / when we add typed throws. throws is the right tool when that is what you want to do. Result will generally be used in other cases, or as you mentioned earlier, possibly when the Result is immediately handled.

1 Like

Yeah, that's my feeling as well; my inclination would be to leave Result unboxed and recommend throw-ing anything you want to propagate further than a single frame.

1 Like

I want to clarify that I favor adding Result only to address situations where manual propagation is necessary. Manual propagation will always occasionally be useful; adding async/await (or whatever we end up doing) will surely eliminate most of the most common situations, but they'll still exist, e.g. when using futures (which are the equivalent of manual propagation for async/await and accordingly trigger similar requirements).

I will continue to recommend against using Result as an alternative mechanism for ordinary propagation of errors, although I have no doubt that some people will insist on doing so; and I continue to believe that using a different error type than Error is generally misguided for the same reason it's generally misguided with ordinary error-handling. I also worry that the ergonomics of this type are significantly hurt by the attempt to support typed errors and (especially) non-Error-conforming errors.

But as someone who's thought quite a bit about error propagation, especially with regards to async/await, I have no doubt that Result will be a useful type even in that future.

24 Likes

I agree with not using Result as the primary error propagation mechanism, and I think that could be part of the Swift language documentation on error handling.

My implementation with a generic Error that's unconstrained was done for a few reasons:

  1. It's the most flexible implementation, allowing the language to grow and change (typed errors, throwing non-Error types), while at the same time allowing use cases that can't be handled at all by the current system.
  2. In the previous discussion thread (March 2018 timeframe), the proposal of an unconstrained Result was seen almost uniformly positively, as it cut the stalemate between the typed and untyped camps while unlocking previously impossible capabilities (non-Error error types).
  3. The unconstrained form would allow easy aliases for both typed and untyped implementations, making transitioning from community implementations to the official one easier. Given the sheer amount of usage, this seems like a significant factor to me, though of course its importance is relative.

I've largely stood back from the discussion after the first bit, to see what responses brought up. But I did want to mention and reiterate a few things.

  1. I largely aligned my implementation with common community implementations, in order to ease the transition build off the common knowledge of the type. However, I do think that changing the generic types to Success/Failure and their value accessors to success/failure would be acceptable, if those names are considered better. I found it hard to find guidance from the Swift naming guidelines in this case.
  2. I wanted to build out a base level of convenience functionality that was common to the community types, as well as to types in the standard library, which is why it focuses mostly around map and other transforms. However, again, there are subtle differences between implementations and not a lot of guidance as to the most technically correct form some of them may take, so changes can be made if it's felt there are more proper forms. However, I do think this is a good base level of functionality that would allow the community to move to the standard implementation quickly.
  3. If I understand the error manifesto (and other related discussion) correctly Error was not intended to be the end all be all error representation in Swift. It was chose and implemented the way it is in order to ease interop with NSError, as well as work easily with the automatic error propagation of try/catch. So I don't believe that all error handling in Swift should be limited to Error conforming types, even if that representation should form the vast majority of errors in Swift. This sort of simple/complex symmetry is reflected in other areas of Swift as well, so I don't believe it's unique to Result.
2 Likes

FWIW, I don't agree that the generic types should be called Success and Failure or that the properties, if properties are included, should be called success and failure. The generic parameter of Optional is not called Some, and members that access it (like unsafelyUnwrapped()) do not refer to it as some.

My reading of the error handling rationale is that Error is intended to at least include all "recoverable errors", which I think is what Result is intended to express. It even discusses converting universal and logic errors into Errors.

IMHO, Error is not a particularly limiting protocol and there's no strong reason to avoid requiring it. The only limitation it places is that types used as errors must be declared to be error types—you can't just decide that you feel like throwing a String or a Void without publicly declaring that those types can be thrown. So allowing non-Error failure values just doesn't seem to me like a kind of flexibility we should care about.

(If we can't make Error existentials conform to Error, that is definitely an obstacle, though.)

4 Likes

Personally, I think an unconstrained Result<T, E> makes more sense, especially if we envision some error handling improvements and more magic happening around async/await, which will cover some of the use-cases of this type. It is not too hard to make a constrained typealias and use it exclusively getting access to all extensions that are conditional on E: Error; going in the other direction seems impossible without introducing a whole new type.

The name Result (and case names, too) seem to imply some sort of error-handling use. Rightfully so, but at the same time it limits the discoverability of the type for other uses. Who would reach for Result when they just want a general container of one of two things? Either sounds better for that, although I'm not a big fan of the usual right/left case names. Ideally I'd prefer Either<One, TheOther> as a "base" type and Result<Value, Error: Error> as it's subtype, with the cases renamed, which sounds impossible, at least now.


The important aspect that's missing from the proposal (and almost missing from the discussion) is that of code migration. It is great that the Result type as being proposed borrows best practices from existing and widely used implementations, but slight incompatibilities will make it not-quite-a-drop-in replacement. Manual migration will not be hard, but it will have to be done, and that's an annoyance. We have been trying really hard for the past couple years to not break existing source code. Even though we can't guarantee it in the presence of any user code, in this case I believe we should try.

One possible solution would be to make the newly added type available only starting from Swift 5.0, so that all the existing uses will not become ambiguous; and then upon migration fully qualify all the mentions of the other Result. This way migrating the project to Swift 5 will not immediately require changing the code to adopt a new type. @akyrtzi might have better ideas.

1 Like

@moiseev this seems like a good idea to me.

2 Likes

I didn't realize the migrator could support this case. Would anything be required on the type implementation side to support this, or would it be something separate? I'd be willing to give implementing a migrator a try, if there's a guide.

I think a migrator that tried to recognize the common cases of third-party Result types would be really useful here. That seems like a reasonable precedent to set for whenever we pull common third-party libraries into Swift: we want to make it easy to adopt the standard solution, but we don't want Evolution to be hamstrung by a need to be drop-in source-compatible with the existing library.

Of course, I'm volunteering work for other people here. :)

2 Likes

For the open source libraries that are willing to accept changes like that, it should be possible to simply annotate their Results with @available(swift, deprecated: 5.0, renamed: "Library.Result"). Unless I'm mistaken, that should result in a compiler warning and a fix-it while migrating. Obviously, that won't work for the user-defined Results.

2 Likes

Please add a typealias like this:

public typealias Continuation = (Result<Value, Error>) -> Void

This way methods taking completion handlers can be written:

func aMethod(continuation: Result<SomeType, Error>.Continuation)

Instead of:

func aMethod(continuation: (Result<SomeType, Error>) -> Void)

Besides that I am neutral (± 0) on the proposal. Result objects are somewhat low level and not used much once we move to Promises and async/await. Result objects might be used to implement Promises though.

I agree 100% with @nicklockwood’s evaluation. I very rarely comment on evolution proposals but I feel so strongly in favour of adding Result that I had to show my support.

I cannot imagine working on a project without a Result type. The implementation as proposed closely matches the implementation I most most commonly use.

I have been following the discussion about adding Result to the standard library for some time.

FWIW I’m -1 on this.

The proposal is well-written and clean and, if a Result Type should be added to the standard library I can imagine it being along these lines (though maybe with a little less API and a little more internal naming consistency) BUT, I personally don’t see any value in a Result type at all going forward, and mirror others’ arguments in that I believe it will complicate Swift’s error handling unnecessarily.

The main proposal seems to be “try/catch doesn’t work async”. Excuse my bluntness but why don’t we “just” make try/catch work with async? I’m going to be unpopular here for bringing up (horror) JavaScript but aside from some hairy edges I quite like how they’ve adapted try/catch to work with async/await in recent language versions.

If the Result syntax is useful in making up for current shortcomings but is mainly a stepping stone for async/await or actors or whatever concurrency primitives we may end up with, why not leave them as an unexposed implementation detail once that proposal lands?

I think cleaning up other projects’ use of their own Result types - which has also arisen due to lack of first class concurrency - should explicitly be a non-goal here. Let’s not end up like C++ with multiple imperfect ways of achieving much the same thing.

11 Likes

What is your evaluation of the proposal?

I’m in favor of adding the missing basic type that sits somewhere between Optional and tuple, and represents a value of either one generic type OR a second generic type. So basically a Haskell Either type as has been mentioned a couple of times. I’m not in favor of spelling this type “Result” or naming its cases things like “error” or “failure” or “success”, which are much more opinionated and limited abstractions than an “Either” type.

  • Essentially every benefit and use case of a Result type is equally served by a more abstractly named Either type. However, the reverse is not true, i.e. a “Result” type with cases that imply only success / failure or value / error are not abstractions that serve well for all places where an Either type could be useful.

  • Naming this type “Result” and having an “Error” case or generic type name strongly suggests that this should be used for error handling and / or propagation. As others have noted, I’m not sure that’s in line with where Swift is going and it essentially leaves a sort of ambiguity. Without Cocoa Touch and other Standard Library APIs making use of Result, it feels like a tacked-on oddity and somewhat dead on the vine.

  • On the other hand, having the equivalent of a Haskell “Either” type would be useful regardless of future error handling and Async/Await implementations. And I could easily see such a type being used appropriately in first party APIs just as Optional and tuple are today. In other words, an Either type is a fundamental concept and a generalized enough abstraction that I don’t think it would be controversial to add to the standard library, and it would clearly be useful across many different scenarios without getting boxed into an ambiguous role as being an unofficial (and clearly unsanctioned) alternative to throws. And assuming an Either type did make it into the standard library, 100% of the uses and benefits of this proposed Result type could be accomplished using it, satisfying that need for those who want to use that kind of abstraction for their own frameworks and API.

  • Not to get TOO philosophical, but the naming of typical Result types and their cases still subtly encourage happy-path only coding and make it only slightly (if at all) more difficult to ignore errors. And this proposal even goes further down that path with additional API that favors only checking for success . A big part of the conceptual rationale for Result is that it’s vital for code to check and handle the error case as being equally possible and valid as the success case. And that rationale is better served by a naming like Either, which makes it clear that a result isn’t going to be (probably) a success or (hopefully not) an error or failure, but rather one of two equally important and equally valid values.

  • The proposal quickly mentions and then dismisses an Either type in the alternatives considered section, with the reasoning for discarding this possibility being that there is an indepedent open source Either library for Swift which has far fewer downloads than the leading Result library. I don’t find this reason for dismissing the Either alternative compelling.

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

Lacking a basic functional abstraction like Either is definitely a significant problem, made evident as noted by how many frameworks and apps have rolled their own version of it. However, I again want to point out that I see the problem as not having an Either type, not as lacking a “Result” type, which is a more limited naming and conceptual abstraction.

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

Again, having a type that can function as proposed fits well, but I don’t care for the naming, which I don’t think fits well. Swift has adapted Haskell’s “Maybe” to “Optional”, and I think it would be completely logical and consistent to adapt “Either” to something equivalent as well. The proposal even avoids constraining the generic error case type to conform to Swift.Error in order to let Result be used for other scenarios that don’t involve error handling. But if we recognize that such a need exists, then we shouldn’t keep the Result / Success / Failure / Error naming at all.

This is subjective, but I can imagine various syntactic and compiler sugar for an Either type being added to Swift just as was done for the Optional type. And I can imagine the Standard Library and Cocoa API that would make use of a generalized Either type, just as they do with the Optional type today. But I find it hard to imagine consistent and widespread adoption of a Result type in Cocoa or the Standard Library.

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

I prefer the Either type in Haskell. It feels like a foundational computer science concept and data type that is both self explanatory and non-controversial as a building block for all kinds of code. A Result type on the other hand is controversial, less abstract, more opinionated and yet not in any way more capable or correct.

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

Quick reading through the whole proposal, and I read the 137 comments currently on this thread.

2 Likes

Soft -1.

I am reluctantly open to a Result type in the Standard Library. This one makes trade-offs I'm not thrilled with.

The Standard Library should indeed seek to capture near-ubiquitous patterns. Though, I would rather investments continue to be made into the Swift error handling rather than undermining it. Swift's feature domains are meant to compose well, provide benefit to the programmer, and meet Swift's goals as a language. If error handling is not meeting that standard, that's a problem.

Now, that's all pie-in-the-sky. The future isn't now, now is now, and Result is very common. This is a problem worth addressing in the language, for now, as sad as I am about needing to commit to a simple two-case enum in the ABI.

On a few points, I don't feel like it fits well with Swift's feel and direction. Sometimes, it's like trying to change that direction by fiat. The proposal mentions throwing not-Errors as a future direction a few times, and I'd absolutely call that a non-goal. Likewise, and more significantly, I strongly feel like the Error generic parameter argument to Result is undesirable and fighting against the language.

As an aside, I'll recall my post earlier that the proposal doesn't meet its own goal of eliminating duplication in third-party libraries because a generic doesn't compose as a where clause. Not the proposal's fault, Optional has the same problem. I mention it here because I suspect the needless ResultProtocol clones is what lead to fold.

Some drive-by notes that I don't feel strongly about:

  • Not a fan of unwrapped(). It doesn't describe what it's doing meaningfully, nor what it's returning.
  • I agree with the monadic methods having their parameters be throws. rethrows is arguable, I think the desired behavior is to re-wrap but I'm not 100% sure.
  • mapError and flatMapError aren't compelling names. I don't think either name is something you'd reach for naturally and they lack symmetry with the non-error variants.
  • fold is borderline-silly. We don't need a function call to abstract over switch. We already have switch, it's great.
5 Likes