Adding Result to the Standard Library

I disagree with the assertion that Either is a common pattern. Usage of Result libraries is greater than Either by several orders of magnitude. To me, that pretty conclusively proves they type should be Result even if it doesn’t have the error value constrained to Error.

1 Like

The argument that Either should be variadic is a compelling one, and I withdraw my suggestion. :+1:

Either is typically the sum-type equivalent of Pair, which is a tuple that's limited to 2 fields. I don't think Either is very interesting—I'd rather see a sum-type equivalent to tuples.

But Either also isn't a very good way to model Result, or any other sum type with 2 fields. I suspect you wouldn't model Point with a Pair<Float, Float> or a (Float, Float) most of the time either. point.0 or point.first is much less clear than point.x. Adding semantic names, which makes the abstraction less generic, is actually helpful.

(Similarly, you could replace Optional with typealias Optional<Value> = Either<Value, Void>, but I don't think that'd be a good idea)

2 Likes

Most of the times I've reached for an Either type is when a function can return two possible types of values, and you want to do pattern matching on that. Granted most of the time that's probably better expressed as a specific enum type for each case rather than a generic Either type.

@DeFrenZ proposed that Optional could be modeled on Result<Wrapped, Void> (a result without any information about the failure case).

I find it interesting. It's an excellent illustration why forcing E to adopt Error is not obvious. You can have a Result<T, E> type, where the failure case is not an error, and that is still semantically sound.

1 Like

If the Either cases were named result and other, rather than left or right, then it would feel more natural to typealias to Result.

That would be assigning specific semantics to Either that don't apply to that general type.

2 Likes

What about (x: Float, y: Float) though?

I believe that if Result is to be added to the standard library it should simply be nominalization of the structural type that is the return value of a throws function, like Optional is for T?.

  • The keyword catch should be usable similar to try but nominalizes the result of enclosed expression into a Result value, instead of resolving it in the current context.
  • Result should have a generic error parameter, if only so that T can be interpreted as a subtype of Result<T, Never> explicitly, and not directly to, effectively, Result<T, Error>, also meaning Never should conform to Error
  • I do not think one should switch over a Result, I think instead it should be Callable to resolve it as if it were thrown from a function.
// dummy code to show my syntax ideas
func someThrowingFunction() throws -> Int { return 3 }
func returnOrPrintError<T, E  : Error>(_ result: Result<T, E>) -> T {
  do {
    return try result() // unwrapping/resolving a Result as throwing
  } catch {
    print(error)
  }
}

let result : Result<Int, Error> = catch someThrowingFunction() // wrapping a throw into a Result
let mapped : Result<String, Error> = result?.description // mapping a result
let value : String = returnOrPrintError(mapped) // result as parameter

I said that almost as a joke really, but it does make sense.
The primary reasoning there is that almost every functionality we have on Optional, a Result can support as well (since they're both monads). With an untyped Result, you could easily write stuff like myResult?.foo() and the very same behaviour would fit (as ?. is really just map/flatMap).
Though I never saw a clean monadic implementation for a typed Result that could support something like resultWithErrorA.flatMap(funcThatReturnsResultWithErrorB), since there is no support for union types in the language (yet?).

I would generally be in favour of typed errors, but until the language makes working with them pleasant I'd prefer to stick to untyped ones.

I agree with you. This could be a great way to slice through this discussion and turn a long philosophic argument into a short and practical discussion.

-Chris

3 Likes

+1. I remain a fan of turning builtin compiler magic into user-extensible hooks. Generalizing optional chaining to work with any type that conforms to a new protocol would be the right way to get Result to support it IMO.

That said, all that is orthogonal to the discussion of whether or not to add Result, and with which design. We can cross those bridges when they come up.

-Chris

7 Likes

I don't see why an E: Error constraint would be important. Does anyone else?

4 Likes

Good point. There's no particular reason that failure has to be associated with an Error-conforming type.

Some people prefer a Result API that returns the value or throws. If we wanted to make an API like this available for all Result values we would need to have an Error constraint.

Stepping back, is there a reason Error conformance is required for throwable types? I don’t recall ever seeing the rationale for that design decision and it seems relevant to this discussion.

1 Like

I mostly want to always have E: Error, but I wouldn't mind having a Result that doesn't enforce that: I can simply use it with the desired types and have the relevant extensions where E: Error.

I don’t think Result needs to be tied to error handling. It’s useful to be able to interoperate with throws, but a two-argument Result would be useful for clients regardless, and maximally future-proof.

If it is an open-ended Result<S,F>, people will type to a particular error subtype. It will become a language-endorsed method to constrain protocols and types to require typed errors.

As someone firmly in the anti-typed-throws camp, this worries me. I personally have seen too many languages where attempts to institute an ontology of errors and to constrain the interfaces to specific error types has just bred complexity and anti-patterns.

6 Likes

I really like this! I don't know where I stand on typed throws, but I agree this makes sense for Result.

I wonder if it would be possible to allow default types for the last type parameters in a definition?

enum Result<T, E = Error> {
    case success(T), failure(E)
    /* etc... */
}

Then you could just provide T in most cases:

 let x = Result<T>(...) //This is actually Result<T, Error>

but you can still be explicit when desired:

 let x = Result<T, MyError>(...) //This is type Result<T, MyError>

or even use a non-error failure object:

let x = Result<T, MyFailureObj>(...)

Because type parameters are organized by position as opposed to name, this would only be allowed at the end of a set of type parameters. Once a parameter provided a default, each following parameter would be required to provide a default as well (or the compiler would complain).

3 Likes

I bring it up mostly because implementation now precedes proposal and it may make sense to pitch it first and then build Result using it for "act on success, ignore error" coding (okay, I know, not the best way to do this but try? exists and a biased Result unwrap would act the same)

1 Like

As another person firmly in the anti-typed-throws camp, I want to re-iterate this:

2 Likes