SE-0235 - Add Result to the Standard Library

I generally agree, of course the devil is in the details, isn't it! :slight_smile:

I thought we were going to need to decide this before a Result proposal would make it to review, but here we are. I actually think that's ok. I am also fine with making the decision now as long as it is decided in favor of #2. :slight_smile:

I would strongly oppose a Result that did not allow for typed errors. That removes flexibility that is occasionally extremely useful. I would prefer to not have Result in the standard library if it were limited to the untyped variant. The typed design allows people to simply use Result<T, Swift.Error> to recover the behavior of an untyped result (and even introduce a typealias if desired). The untyped design forces those of us who find typed errors useful to fight the language one way or another. The current state of choosing third party libraries or rolling our own is much better than having to fight the language.

This was discussed extensively in the pitch thread. I wish you would have participated there!

Adding throws to transforms on Result is categorically different than it is on other types, such as collections and optional. The version of map you advocate for is a syntactic transformation of flatMap semantics. I think it's important that the name reflect the semantics of the operation. Using map here does not feel right to me.

Further, in Swift 5 (where this would be accepted) your map only works when Error == Swift.Error but surely we want map to be included without constraints. Your version of map could be included in a constrained extension along side an unconstrained non-throwing map. That would be a simple renaming of the throwing flatMap in the proposal.

Of course this would be subject to the concern @John_McCall brought up a concern regarding overloading only on whether the transform throws or not. I am unsure how often problems would arise in practice - people are unlikely to want to return Result from the throwing overload and must return Result from the non-throwing overload. However, the bar for acceptance in the standard library should be very high and the potential for problems with this overload is certainly present. I now believe throwing transforms should not be included if the proposal is accepted.

I don't believe there are good alternative names for the methods with throwing transforms. People could bikeshed if desired, but very much doubt anything remotely resembling consensus would emerge. It is trivial for those who want them to add them in extensions. If substantial experience proves that the overload is not a problem perhaps they could be proposed down the road.

Assuming we did have typed throws, the type signatures you proposed would perhaps make good replacements for Result-returning flatMap and flatMapError methods in the proposal. However, the names should remain the same. The semantics of flattening remain and are distinct from a simple map.

This issue is significant in the case of your mapError which returns Value rather than a new error type. When users actually just want to map the error they would be forced to provide a function which always throws. That is rather unusual and certainly unprecedented in the standard library.

I see no good argument for this whatsoever. This introduces jargon for jargon's sake. Swift programmers are becoming quite familiar with map and it's variants - flatMap, compactMap and mapFoo (where Foo selects part(s) of an aggregate).

This change happened for good reason - the method now named compactMap was never semantically a flatMap and that caused plenty of confusion. Another name that was considered for this operation is filterMap which expresses the semantics of the operation rather clearly - it is single operation that both maps and filters, just as flat map is an operation that both maps and flattens. We definitely should not introduce new names for an operation that is semantically equivalent to a flatMap.

I strongly oppose including an initializer like this. The use case is common enough but there simply is not a design that is obvious enough to warrant inclusion in the standard library. It is impossible to know a-priori how to handle the two nil case, and especially the value and error case. If this initializer is included in the standard library people will use it whether or not it is appropriate for a specific use case.

4 Likes

Without the need to sacrifice any flexibility here, I think it‘s fair to say that #2 is just the right way to go regardless wether we will support typed throws or not. #2 is also just perfect for all people that do support only #1. The trick is that we want to support default types for generic type parameters in the future, we just don‘t have them right now!

So the proposed result type will eventually become both #1 and #2:

public enum Result<Value, Error = Swift.Error> { … }

Edit:

I would like to extend the idea even further. @Slava_Pestov what if instead of letting the Error protocol conforming to itself, we consider a slightly different approach by allowing a new type of constraint for where clauses?! Furthermore I think making Error conforming to itself might shoot us in the foot in the future where we won‘t be able to implement a feature because of that exclusive exception on how protocols behave.

Instead we may allow a new constraint which describes that a generic type parameter either conforms to a specific protocol or is that protocol existential. Let‘s make it :== for now.

With this constraint we don‘t have to add hacks like self conforming protocols, but we potentially lose covariance part of your pitch.

// Error defaults to `Swift.Error` if it‘s not specified explicitly and `Error` must
// conform to `Swift.Error` or be `Swift.Error`. 
public enum Result<Value, Error = Swift.Error> where Error :== Swift.Error { ... }
3 Likes

What is your evaluation of the proposal?

Big +1.

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

Yes, at the moment this is a big problem that's finally being addressed. I think most people here in this thread are looking too much into the future. Yes, we will have maybe have a better solution a couple of years down the road but does that mean that we have to write mediocre code and end up with a bunch of different implementations in every project? This is the main reason why I think this proposal should be accepted with the current state of Swift.

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

Yes, as it can be seen from the tens of thousands of people that implemented this solution in their own projects.

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

Haskell uses a very similar way of handling two possible exclusive values with Either. Writing code in Haskell for a short amount time during my academic time made me fall in love with the way that a language can enforce writing correct and simpe to understand code. I think Swift would only benefit by adding accepting this proposal.

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

Read the proposal, read the first 50 replies to this thread and skipped the remaining since there are too many. :+1:

I think anything added to the Swift standard library right now should only be provided as a way to propagate errors across different execution contexts / asynchronous callbacks. i.e. something more analogous to C++'s std::exception_ptr rather than a full-blown Try type. And even for this use case I would prefer to wait for development on the async/await front.

Sure, other languages have a mixture of Try/Result types as well other error handling mechanisms, but the proposal's examples aren't very compelling (except for the asynchronous case) and don't do a great job of explaining how Result and do/catch/try will co-exist. Will people use Result to entirely co-opt the native do/catch/try error handling mechanism? Is this going to become the next "exceptions vs. return codes" discussion (or rather, "Result vs. do/catch")?

The lack of language integration, or discussion of this in the proposal is also a concern: if language affordances are to be added later, shouldn't some initial examples/design be described in the proposal to ensure the type is designed with them in mind?

Also, is there a plan for any automatic bridging of ObjC APIs? Or annotations to map completion callback arguments to Result arguments? The URLSession example certainly highlights real problems, but will the addition of Result actually lead to improvement in any of these existing APIs? And what happens when async/await comes long?

3 Likes

Ouch, Soroush, tell us what you really think :wink:

My experience is basically the opposite: I use Result all the time and have never used Promise.

However, I don't really care if this proposal makes it into the standard library or not. As I mentioned in my article Values and errors, part 1: 'Result' in Swift, Result is a trivially simple type and translating between implementations is easy and rare so putting it in the standard library isn't really necessary.

I'm far more worried about negative impacts this proposal might have on runtime performance if 2 generic parameters must be resolved during Swift type metadata lookup. All I can request is that Swift devs perform tests and offer assurances that Result<Value, Error> will be equivalent performance to Result<Value> if the Error type is hard-coded to be Swift.Error – including in cases where the Value is unspecialized and not known at compile time.

10 Likes
  • What is your evaluation of the proposal?

+1

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

Very much! I'm using it in all my projects and very often I'm dealing with multiples implementations at the same time which is unfortunate. I would love to see universal solution provided by the standard library.

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

Generally very well. I'm very much in line with @brentdax thoughts here, but in a strong favour of typed errors.

Although in most cases they are not need, there are occasions when typed errors are very useful. I've stumbled upon such cases many times and I would rather not have Result in the standard library than have one with untyped errors. That being said, I'd also love to have Result that is simple to use with untyped errors, like Slava suggested by making Swift.Error conforming to itself. I think this would be a big win, not just for Result, but also for other types like Futures or Signals.

In addition to that, I'd also love to see Error generic parameter defaulted to Swift.Error. In other words, my ideal Result type would be defined like: Result<Value, Error: Swift.Error = Swift.Error>. I know this is not possible now because default generic parameters are not supported, but they are in the manifesto and have their usefulness so I hope to see them supported eventually and then the whole discussion about typed vs. non-typed error becomes pretty much pointless. Therefore, let's us not constrain ourselves to untyped errors.

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

In-depth study.

Hello,

Does the proposal stand the most basic of the test, which is, at the bare minimum, to deal with our CURRENT error system, which is untyped?

I've built a playground in order to assert that the untyped flavor of the proposed Result can be used in basic situations, and see how it behaves in some corners: https://github.com/groue/SE-0235. This playground uses the Result type as shipped with the latest version of https://github.com/apple/swift/pull/19982

  1. Generally speaking, it is quite OK. I could assert that a fairly large deal of normal usage can be done.

  2. There is a cost, which is the definition of a typealias. I've used AlamofireResult, because Result<T, Swift.Error> is close to the current Alamofire.Result, which is probably the most used untyped result type out there. If anybody has a better idea, please speak up :slight_smile:

    The topic of this typealias is important: I would not be happy having to type the verbose Result<T, Error> everywhere, especially considering that I'm not supposed to fight the language when I use untyped errors, which is, I repeat, our CURRENT error system. I don't understand why the proposal wants to punish me, here.

  3. There are two misuses that the proposed Result does not see: https://github.com/groue/SE-0235/blob/2af6102b77caa8d16587bbfa8d8c0a2e55592e7b/Result.playground/Contents.swift#L44-L46 and https://github.com/groue/SE-0235/blob/2af6102b77caa8d16587bbfa8d8c0a2e55592e7b/Result.playground/Contents.swift#L48-L50:

    _ = AlamofireResult.success(1).flatMap { "\($0)" }
    _ = AlamofireResult.success(1).flatMap { _ in throw SomeError() }
    

    Those are interesting, because we recently introduced compactMap in order to fight the same misuse, in the domain of optionals.

  4. Finally, there is one "ambiguous use of 'flatMap' " compiler error https://github.com/groue/SE-0235/blob/2af6102b77caa8d16587bbfa8d8c0a2e55592e7b/Result.playground/Contents.swift#L61-L70 :

    func incrementResult(_ int: Int) -> AlamofireResult<Int> { return Result.success(int + 1) }
    // compiler error: ambiguous use of 'flatMap'
    let result = AlamofireResult.success(1).flatMap(incrementResult)
    

    I can't tell if this is a big problem or not. I, personally, don't write much functions which return Result, and prefer throwing functions. But I wonder what other Swift users think.

Conclusion: when one focuses on the integration of the proposed Result with the current error system of Swift, which is untyped, we see that the proposal generally fits the bill. I could find three issues, though.

6 Likes

That is very interesting actually. The last flatMap that throws is actually troublesome for a few reasons. As implemented where the concrete Error type is Swift.Error it will propagate any possible error that the closure might throw to the new result.

Hypothetical implementation of the counterpart with typed throws:

extension Result where Error : Swift.Error {
  // The transformation would require to throw the same type, because
  // there is no way to transform the current `Error` to something different
  // if the closure would throw a different error type.
  public func flatMap<NewValue>(
    _ transform: (Value) throws<Error> -> NewValue
  ) -> Result<NewValue, Error> {
    switch self {
    case let .success(value):
      do {
        return .success(try transform(value))
      } catch {
        return .failure(error)
      }
    case let .failure(error):
       return .failure(error)
    }
  }
}

After that little mind experiment I don't think this methods can be called flatMap because they're not flattening the result of the closure. The behavior feels like a compactMap indeed.

Interestingly this brought me to an idea of a general map and flatMap methods, which are currently not expressible but make quite a lot of sense. To solve the above limitation where the throwing transformation might return a different type we actually need to add typed rethrows (or just throws for flatMap).

extension Result {
  public func map<NewValue, NewError>(
    _ transform: (Value) throws<NewError> -> NewValue
  ) rethrows<NewError> -> Result<NewValue, Error> {
    switch self {
    case let .success(value):
      return .success(try transform(value))
    case let .failure(error):
       return .failure(error)
    }
  }

  public func flatMap<NewValue, NewError>(
    _ transform: (Value) -> Result<NewValue, NewError>
  ) throws<NewError> -> Result<NewValue, Error> {
    switch self {
    case let .success(value):
      switch transform(value) {
      case let .success(newValue):
        return .success(newValue)
      case let .failure(error):
        throw error
      }
    case let .failure(error):
       return .failure(error)
    }
  }
}

These methods can be very useful and they show that the currently proposed map and flatMap methods are in the way. Therefore I'd like to point out again that we should rename the proposed methods to mapValue and flatMapValue.

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
Terms of Service

Privacy Policy

Cookie Policy