Adding Result to the Standard Library

The ergonomics of such a type are rather poor, since it would require the creation of some sort of AnyError type to wrap every instance an API throws just an Error. Given that all current Apple APIs only throw Error, and that even Swift-native APIs will throw Error, this seems like an anti pattern (unless we want to promote users encapsulating every error in their program into a single type).

In order for the type to really make sense as the manually propagating alternative to throws, I think it needs to match the underlying error philosophy of the language. Typed throws changes that slightly, which is why I think most people want to have a decision there before adding Result. And despite the fact that Result could be used outside of throw, that is one of the primary use cases, so minimizing the friction between throws and Result seems like a good thing.

Error has no Self or associated type requirements (or any requirements), so AnyError shouldn't be necessary, right?

2 Likes

I just meant that, since an Error instance can't be used in a Result<Value, Error> instance, you'd need to wrap it in some strong type. It's not a type erasure box, but a strong type box.

Ah, well the poor interaction between Result<T, U: Error> and untyped throws really boils down to that whole debate about the pros and cons of having typed throws in the first place, doesn't it? If I understand you, you're coming down on the side of Result<T>.

Right.

I wish I could edit my original post in this thread and correct the link, but the PR is here. I haven't been updating it much since the typed throws discussion seems like a prerequisite.

AnyError shouldn't be necessary. Error already works as a self-conforming existential type because it has no contravariant requirements, as well as a specialized representation that avoids the representational issues with other self-conforming existentials.

I'm not sure what you mean, attempting to declare a Result<Data, Error> fails with the usual "Using Error as a concrete type conforming to protocol Error is not supported." error that we've seen for years.

Edit: Rereading your initial response, it sounds like you're talking about Either<T, E> instead of a typical Result. Either has approximately zero usage in the wild, so it seems like it's unnecessary.

Hm, I thought the runtime supported this. Making the Error type conform to the Error protocol should be easier than the general case regardless.

(e) I am definitely talking about Result. I don't think it necessarily needs a constraint on the 'left' side to communicate the bias toward the success case.

Perhaps it would be useful, for me at least, for you to declare your Result type, if it's any different from Result<T> or Result<T, E: Error>.

Something like this:

enum Result<T, E> {
  case success(T), failure(E)

  func map<U>(_: (T) -> U) -> Result<U, E>
  func flatMap<U>(_: (T) -> Result<U, E>) -> Result<U, E>
  /* etc. */
}

extension Result where E == Error {
  init(_: () throws -> T)
  func get() throws -> T
}

// add `extension Result where E: Error` in a future Swift with typed throws

E can have an Error constraint, but it doesn't necessarily need one to be useful.

4 Likes

That essentially Either, but with the extensions, could replicate current Result<T> functionality. Cases make the use case more clear than the left and right of Either too. Interesting. I can try an integration into Alamofire and see if it still works well.

I know this is bikeshedding but coming from the RxSwift world I'd think that those map and flatMap operators wouldn't be flexible in practice since E is fixed and cannot be mapped. Here is the RxSwift design rationale not to use generic error.


Here is also a blog post from John Sundell where he shows an example of Future and Promise types, but which requires a non-generic Result type:

1 Like

I think those examples would work with Joe's Result<T, E>, since it allows any type of Error in there, just like Result<T> does. It just also allows any other type as E, instead of just Error-conforming ones. The result of the UserLoader, for example, would be Result<User, Error>.

OMG. I was pretty sure Swift used to be unable to deal nicely with Joe's Result<T, E>.

But actually it can. All you need is NOT constraining E to adopt Error. If you do, everything breaks because Error does not adopt itself.

I was able to build with Xcode 9.2 a basic micro-library that's both usable, and will please both camps (untyped vs. typed results):

// The "Library"

/// Base type: a typed result.
public enum ResultBase<T, E> {
    case success(T), failure(E)
}

/// When E is an error type, you get `unwrap()`
public extension ResultBase where E: Error {
    // Unwrap loses error information
    func unwrap() throws -> T {
        switch self {
        case .success(let value): return value
        case .failure(let error): throw error
        }
    }
}

/// When E is exactly `Error`, you can build a result from a throwing function:
public extension ResultBase where E == Error {
    // ResultBase<T, Error> can be initiated from a throwing function
    init(_ value: () throws -> T) {
        do {
            self = try .success(value())
        } catch {
            self = .failure(error)
        }
    }
}

// ODDITY: This overload is necessary in order to avoid a compiler error
// "using 'Error' as a concrete type conforming to protocol 'Error' is not supported"
public extension ResultBase where E == Error {
    func unwrap() throws -> T {
        switch self {
        case .success(let value): return value
        case .failure(let error): throw error
        }
    }
}

/// Convenience typealias for untyped errors
public typealias Result<T> = ResultBase<T, Error>

Usage:

// Returns an untyped result:
func f() -> Result<Int> {
    return Result.success(1)
}

// Returns an untyped result:
func g() -> Result<Int> {
    struct SomeError: Error { }
    return Result.failure(SomeError())
}

// Returns an result with typed result:
struct SpecificError: Error { }
typealias SpecificResult<T> = ResultBase<T, SpecificError>
func h() -> SpecificResult<Int> {
    return ResultBase.failure(SpecificError())
}

// Prints "f = 1"
do {
    try print("f = \(f().unwrap())")
} catch {
    print("f error: \(error)")
}

// Prints "g error: SomeError"
do {
    try print("g = \(g().unwrap())")
} catch {
    print("g error: \(error)")
}

// Prints "h error: SpecificError"
do {
    try print("h = \(h().unwrap())")
} catch {
    print("h error: \(error)")
}
1 Like

In case it would have been missed: my message above implies that we can ship a Result type before we settle on untyped/typed throws :-)

And then we would have typealias Optional<T> = ResultBase<T, Void>, and move all optional syntax sugar to results? :P

2 Likes

By not forcing failure to be Error you can write:

ResultBase.failure(ThisObjectIsNotAError())

If it is possible then someone will do it for sure.

I feel like we're making things more complicated than it need to be, even with ABI stability in mind. Why not keeping things simple by simply adding the "usual" Result<T> everyone knows and use?

Then if we need it we will add Result<T, SpecificError> while still keeping Result<T>, and making both compatible with variance/covariance, default type or whatsoever. All will depend on what we will have in Swift at that time.

Plus having to write each time Result<MyType, Error> instead of just Result<MyType> feels too cumbersome to me.

The ResultBase<T,E> above, which does not force E to adopt Error, and allows defining a typealias Result = ResultBase<T, Error>, has two characteristics that one may find good, or bad:

  • It pleases both camps, and pushes the debate away. Prefer typed errors? Use the typed result, and move on. Prefer untyped errors? Use the untyped result typealias, and move on. Need extra convenience APIs that fits your programming style? Write extensions with appropriate constraints.

  • It allows to use any type for the "failure" case, even non-error types. This may be seen as a drawback, or a boon. I think the charge of proof here lies to people who think it's a drawback, and they'll have to show evidence that a result type needs its failure associated value to adopt Error.

It contains both https://github.com/antitypical/Result and Alamofire's Result, which are, unless I'm totally mistaken, the two flagships of typed/untyped result styles.

If the E: Error constraint is important, making the Error type conform to the Error protocol is something we could implement. The Error type's special one-word representation and the existing bridging logic that has to unwrap nested error values in order to preserve NSError identity should make this easier than the general case of protocol type self-conformance.

3 Likes

Rust uses a two-argument Result type, and it looks like a common practice there is to typealias Result for domain error types. You could do the same in Swift:

typealias IOResult<T> = Result<T, IOError>
typealias CocoaResult<T> = Result<T, CocoaError>
typealias AnyResult<T> = Result<T, Error>
/*etc.*/
2 Likes