Adding Result to the Standard Library

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

Yes, this is something that should compose into the generics system. It is mentioned here:

3 Likes

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"
1 Like

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).

1 Like

...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.

2 Likes

With Result<T,E> without constraint on E, Result is not a reified try call. A reified try call is Result<T, Error>.

Several examples of "failure" that are not representable by Error have been shown above.

And one can still write constrained extensions where E: Error or where E == Error.

Don't you think it works pretty well?

3 Likes

But why would you want to do that?

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).

1 Like

Nobody wants to do that. It's just an illustration of the fact that "failure" and Error are not an exact match. Errno can be a failure. Void can be a failure. An Objective-C exception can be a failure. A plain string can be a failure, in some contexts.

And if you want to use those in a Result, you should wrap them in an Error type. If you want to use any of those with Swift's throw, you'd need to do that too.

Result.failure isn't meant to represent every idea of failure. It's meant to represent failure in the same way that a try call can fail.

8 Likes

the heart of the matter is that Optionals, throws, and Result are about error handling. A unified error handling mechanism?
Ive seen this come up before in this list. It always gets to the point that everybody is pretty much deadlocked in a tug of war between Result, Either and Typed throws.

If we want Result to be added then I think we focus on Result, and the figure out a way make it compatible with Optionals and Throws later. Otherwise nothing will happen.

Imagine being able to use the optional facilities to wrapp values in a result type. I think that would be awsome.

Optional isn't about error handling. It's been used for that, but it's fundamentally about data that may or may not be there. That's much broader than error handling. And Result/throws are useful because Optional isn't actually very good at error handling.

6 Likes

Just throwing a few IMHO points out there.

  • Either is a semantically-erased unbiased coupled enumeration
  • Result is biased and semantically-driven
  • Result shouldn't be tied to Cocoa's historic NSData, NSError structure solely for the sake of historic trend.
  • Arbitrary typing enables both success/error handling as well as success/fallback. I would name the branches (and the generic type arguments) something along the lines of enum Result<Expected, Unexpected>{ case expected, unexpected } or maybe (thanks Brent) Primary, Alternate.
  • Adding Result should not exclude Either.
3 Likes

That seems like a great argument for Result<T> over Result<T, E: Error> then, given that Swift doesn't have typed throws.

You're right. And this is why I don't understand why the unconstrained Result<T, E> faces so much resistance.

People in the untyped result camp can define the typealias they need, and define extensions on Result where E == Error in order to define their convenience methods:

typelias UntypedResult = Result<T, Error>

And people in the typed result camp can just build the Result<T, MyError> values they need, and define extensions on Result where E: Error in order to build their own convenience methods.

I don't buy drastic sentences like "failures are fundamentally errors", because they have no effective consequence. Extensions on Result where E: Error and Result where E == Error, on the other side, do have effective consequences.

No, it's a great argument for what the core Swift team has repeatedly communicated: That Result can't be added until we know whether Swift will get typed throws.