Adding Result to the Standard Library

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.

That would be a consistent position to take. You want to tie Result to Swift's existing error handling mechanism, such that in the case of .failure(T), we have T == Error if Swift has no typed throws or T: Error if Swift does have typed throws.

The alternative--and I think what @Chris_Lattner3 and others are getting at--is that Result can be entirely orthogonal to throws, because there's no semantic reason that failure has to be only the kind that throws.

2 Likes

I agree strongly with this. Optional has two main uses:

  • Indicating the expected absence of a value, like Dictionary.subscript
  • Indicating the inability to produce a value, like Int.init?(String, radix: Int), when there is only one possible failure case (the string is either a valid integer of the receiver's size in that radix, or it's not)

I kind of consider the second situation to be an improper application of Optional and think there would be value in having those initializers be throwing instead. The error could pass back additional information that must already be known anyway, like the index of the character that caused the conversion to fail.

The nice thing about Swift's error handling is that try? lets error cases degrade very nicely to Optional cases when all you care about is "an error occurred or not, but I don't want details". In other words, it puts the power in users' hands to decide whether they want to handle error cases as errors or optionals, rather than the API forcing that choice on them.

In general, I think discussions like "Optional<T> could be represented as Result<T, Void>" are red herrings because even if that's algebraically a correct mapping, we wouldn't want it because the case names and associated values provide important semantic value that would be lost if we reduced everything down to typealiases of the most general type. It's the correct thing to do to have these semantically unique cases treated distinctly in the type system.

1 Like

Swift's error handling rationale describes the different kinds of error and why simple domain errors are modeled as Optional results:

Conditions like this are best modeled with an optional return value. They don't benefit from a more complex error-handling model, and using one would make common code unnecessarily awkward. For example, speculatively trying to parse a String as an integer in Java requires catching an exception, which is far more syntactically heavyweight (and inefficient without optimization).

Yes, I'm aware of that, but that doesn't mean I agree with it. :blush:

In which of the two cases would Optional.none not semantically mean some sort of failure?

Well, let's not go bikeshedding the entire error handling design of Swift here now. That's how we get nothing done.

2 Likes

I asked upthread about the rationale for restricting throws to types that conform to Error and haven’t gotten a response yet. There is no fundamental semantic reason for that restriction that I can think of either. It is fundamentally a control flow mechanism.

Why aren’t we allowed to say throw “error message” or throw 42? There are plenty of reasons why this isn’t a good idea (at least most of the time), but if there is a technical reason for the limitation it isn’t obvious to me. I am not opposed to dropping the Error constraint from the failure case of Result but I would like to have a better understanding of why it should be treated differently than errors that can be thrown.

2 Likes

Why do so many people want to use Result for something else than its primary use case, which is reifying errors and various other failure cases, as well as supporting "manual error handling" (mainly in asynchronous contexts)?

I read here and there desires to redesign optionals (or avoid such a redesign), desires to unify error handling with optional handling (or avoid it), rethink try?, try!, !, and why not as? and as!, and other phantasms.

I though that the topic was to provide a ready-made Result type in the stdlib, not to perform deep surgery in the language? At least, no surgery until Result is well-defined?

3 Likes

Thank you for your concern, but I had no intention of doing so. I was merely commenting on it in relation to the discussion about optionals and failure cases.

Let's move on.

I guess it depends on the semantics you associate with "error" or "failure". Some might argue that looking up a value in a dictionary and having it not be found is a "failure of lookup". But I think there's an important semantic difference between "failure" (which I consider to be more exceptional/unexpected cases), and the case where "it's expected that the value may not be there". I realize people will draw the line differently, though.

The worse problem for me is that IMO it's also a leaky abstraction to force the .failure case for an optional to carry a Void associated value.

I don't think anyone's proposing deprecation of Optional<T> for Result<T, Void>. Rather, this was meant to be a thought experiment as to the semantics of Result's failure case. The argument is that it can be, in one design, analogous to the semantics of Optional.none and doesn't have to be tethered to throws.

2 Likes

So would you recommend adding Either as well as Result in the stdlib with this proposal? Personally I'm a big fan of the Either type. But I think a few people are going to point out that it's an extremely vague type, and will worry about its usefulness in the standard library.

I agree, the only issue I see is that, depending on what is finally agreed on, you'll have two types that are basically the same, but with different semantics (which I don't see as an issue) Either<L, R> vs Result<V, E>. But I'm sure there are some people see that as an issue.

1 Like

Right, but just as @anandabits asked above, what makes the semantics of Result's failure case different from the semantics of a thrown error? If it makes sense to not require additional semantics provided via the type system for the failure case of Result, then why does it make sense to require that throw only apply to types conforming to Error?

With regard to the thought experiment, it's true that algebraically Result<T, Void> is equivalent to Optional<T>. But since Void is just a type that contains a single element, then Result<T, NotPresentError> would also be equivalent where enum NotPresentError: Error { case notPresent }. Now I don't think anyone would seriously recommend doing that, but in terms of continuing the thought experiment, I'm not sure that the ability to substitute Void there just because it happens to be a singular-valued type strengthens the case that other non-Error failure cases hold their weight.

1 Like