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 untypedResult, 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 typedResult 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.
+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.
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)
Even though I shared my concerns about the generic E, I noticed a similarity in functionality when the generic E type isn't constrained by Error protocol.
For instance something like Result<String, String> could mean that when a failure happens then you just return a default value instead.
In RxSwift that could look like:
let observable: Observable<String> = sequenceOf("A", "B", "C")
.catchErrorJustReturn("X")
In Swift the analogy is probably the following today:
let value = try? someThrowingFunc() ?? "Default Result"
The benefit here being that you are aware that the String that you are using is a default.
I think that concerns around an unbound .error are unfounded. If they are as terrible as this paralyzing concern would indicate, they will not become widely used. If we gain some form of typed throws, I should hope that it isn't terrible and becomes something that we want to use.
What I see as the ideal version of Result in swift is Result as a specialization of Either where we have gained "enum protocols" where we can indicate case aliases. This would allow us to create a protocol filled with methods to be shared by two-cased enums. Barring that, strong type aliases (newtype) would be another way. If all of those are unattainable, then we could just make Result<L, E> with an unbounded E and it could/would be Either in all but name.
"There’s no particular reason that failure has to be associated with an Error-conforming type.". Ok, so there's something i'm missing, because i was certain Error (and throws) clearly was the privilege way to express failure... and If not then why create the Error protocol in the first place ?
(I've been receiving email from this thread eventhough i hadn't register to the forum yet, but seeing the conversation drift from "we need result until proper async is in the language" to "we need Result as a general Either-like type", made me react).
...some types of failure but not others--for example, ones that cause a function to return nil. As has been said before, Optional<T> could then be modeled as Result<T, Void>.
The whole purpose of this discussion is to create another alternative way to express failure besides throwing an error, so I see no reason why the two have to be forcibly tethered so long as they can be interconverted.
Hm, ok i think i understand what you mean.
In my mind there are three different things :
1 - how we move result of a call back to the caller. That is : throw , or return.
2 - what is the topmost structure of the result : a tuple, a struct, an enum, an optional, etc.
3 - what's inside that structure : User, Tweet, ServerError, MyBusinessRuleError, etc.
In that sense, Result<> would belong to 2/. i agree that the distinction between 2 and 3 is a bit blurry, but it's probably how people design in general (except it's 3 then 2). They start with their business types (including the various errors), then once they start going async or have failable operations, they start grouping them together into convenient structures that let them express whether the operation is globally a success or an error.
EDIT : so, my point is, adding Result wouldn't replace Error per say. It's just another more convenient way to pack types. But the meaning still lies in the 3/ types. If we intend to mean that the second component of a result is meant for failure, then it should still express that fact in itself, by its own type.
Because Result is a reified try call. If you can only throw an Error, then Result.failure should only take an Error.
But more practically, it's often an important part of using a Result. You may want to materialize a try into a Result, but you'll just as often want to go in the other direction and throw from a Result.failure or use other APIs that take an Error.
And while you'll often have a concrete error type that does conform to Error, every generic method must now add a constraint or use as?. IME this makes life difficult as it can be hard to anticipate the need for these constraints.
Optional and Result are semantically different ideas, so I don't see why we'd want to merge them. nil isn't necessarily a failure and some isn't necessarily success (unless maybe if you squint).