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
.
The argument that Either
should be variadic is a compelling one, and I withdraw my suggestion.
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)
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.
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.
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 totry
but nominalizes the result of enclosed expression into aResult
value, instead of resolving it in the current context. -
Result
should have a generic error parameter, if only so thatT
can be interpreted as a subtype ofResult<T, Never>
explicitly, and not directly to, effectively,Result<T, Error>
, also meaningNever
should conform toError
- I do not think one should switch over a
Result
, I think instead it should beCallable
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
+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
I don't see why an E: Error constraint would be important. Does anyone else?
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.
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.
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).
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)
As another person firmly in the anti-typed-throws camp, I want to re-iterate this: