[Revised] SE-0235: Add Result to the Standard Library


(Joe Groff) #21

The proposal does not suggest using Result where throw is appropriate. It will still be more idiomatic and more efficient to use throw and catch where possible. Result is a way of storing potential errors; the idea here is not to promote it as a primary way of handling errors.


(John McCall) #22

I don’t understand the connection between “this is an enum” and “this will replace try-throw-catch”.

There are, without question, programmers who will use this instead of try-throw-catch. We know this because some of them have posted in the pitch and review mentioning that they already use their own Result type instead of try-throw-catch. That we can’t actually stop them from doing that is clear from the fact that they’re doing it now. But for everyone else we can make it clear that we don’t think that’s the right way of working with errors.


(Guillaume Lessard) #23

If it's not an enum it's obvious that switching over it isn't the preferred way. If the easiest way to decompose a Result is in a do-try-catch construct, I think it would be better. If it's an enum, then most people are likely to reach for for a switch first.


(Jordan Rose) #24

I also don't understand the "switch is co-equal to do-try-catch". The main feature of do-try-catch is that you can have more than one try in the same do block, and you don't have to produce a result value to use it.


(Michel Fortin) #25

One could say that this is all you need:

do {
    try handleValue(result.unwrapped())
} catch {
    handleError(error)
}

Whereas if the type is an enum you can also write this:

switch result {
case .value(let value):
    handleValue(value)
case .error(let error):
    handleError(error)
}

Offering too many ways to use the type could be wrong if you look at it from a certain angle, I guess. So maybe Result should be a struct that privately uses an enum for storage but doesn't expose the enum directly. I think this was the idea behind the comment.

And I think it's something to think about, but I have a feeling it's going a bit far in hiding the implementation details.


(Yuta Koshizawa) #26

I agree on the changes. In particular, it looks great to me that the Error type parameters is constrained to conform to Swift.Error, with self-conformance, and the Result type has minimum APIs.

I want a guideline when to use the Result type. Opinions seems divided even in the Core Team as follows.

If I correctly understood, it seems like @Joe_Groff suggests that JSONDecoder.decode(_:from:) returns a Result and @John_McCall does not. I think errors from JSONDecoder.decode(_:from:) are something between simple domain errors and recoverable errors defined in "Error Handling Rationale and Proposal". Those errors are caused by some different conditions. nil is obviously insufficient to represent those errors. Also they don't require manual propagations. However they are not "systemic errors", and throws/try does not allow us to specify their error type (without typed throws). Should JSONDecoder.decode(_:from:) return a Result?


(Guillaume Lessard) #27

Of course. But when one has a Result on one's hands, how does one access its innards? If it's an enum, I believe most people will reach for a switch, and I'm not convinced that's the right outcome.

Here's another way to pose the question: is making Result decomposable in a switch statement essential to its usefulness? I don't think so.


(John McCall) #28

I mean, without pattern-matching over an enum that code just looks like this:

if let value = result.value() {
  handleValue(value)
} else {
  handleError(result.error()!)
}

I don't see how that's semantically discouraging in any way. Literally nothing about making this not an enum, or making it harder to do pattern-matching over it, would change the extent to which it supplants normal try-throw-catch error handling.


(David Owens II) #29

Please also include updating the official Swift documentation with how this new type fits in with the overall error handling approach in Swift too.


(Tony Allevato) #30

I didn't chime in much during the original review period, and I've also been pretty hesitant/skeptical about the value of Result when compared to Swift error handling and future async capabilities.

With that in mind, I hit a situation very recently (doing some recursive processing with SwiftSyntax's SyntaxVisitor) where I needed to account for failure cases, and the visitor's methods aren't declared throws-ing. Instead I would need to store "either the result or an error" as state to retrieve later, and well, Result<T> is precisely the way to model that.

Situations like this will surely continue to arise where we need to integrate with APIs that make it difficult or impossible to use traditional Swift error handling, or where the value-or-error outcome of a computation needs to be stored in a more long-lived fashion. In those cases people will reach for a type like this, so I've warmed up to the idea of the standard library providing a vetted implementation and best practices around it. I'm very happy with the above changes to the original proposal (especially the Error constraint, which I feel is absolutely mandatory).


(Michel Fortin) #31

I think the idea is to make such things difficult by not providing value and error accessors, only unwrapped. This is a way to make a statement about the right way to use Result: you always unwrap and have to handle the throw. I think it's a valid design, but I worry it'd be a bit too strict.


(John McCall) #32

Oh, I see, that does make sense. Yeah, I don't think that's a direction we'd want to take.


(Guillaume Lessard) #33

Now I know the discussion has been had at least a little bit!

I would prefer a more restrictive shape, as nicely expressed by Michel, but I'll be happy to have a standard Result regardless.


(Joe Groff) #34

The easiest way to get the value out of a result with this proposal seems to me like it'd be to use the unwrapped() method rather than pattern matching with switch, which will give you a value you can use locally in expressions or else throw the captured error to be handled by catch somewhere.


#35

Example about URLSession in revised proposal includes a wrong.

URLSession.shared.dataTask(with: url) { 
    (result: Result<(URLResponse, Data), Error>) in // Type added for illustration purposes.
  ...
}

This signature of closure has lost URLResponse when error happens.

Source:

https://developer.apple.com/documentation/foundation/nsurlsession/1407613-datataskwithrequest

If the request fails, the data parameter is nil and the error parameter contain information about the failure. If a response from the server is received, regardless of whether the request completes successfully or fails, the response parameter contains that information.


(Guillaume Lessard) #36

I hope you're right. I agree (obviously) that unwrapped() is clearly the better way, but I doubt it will be preferred in practice; in fact I expect it will be used in a minority of cases. I must be a pessimist.


(David Waite) #37

Could there be a conditional version of map which takes a throwing transform when Error == Swift.Error?


(David Waite) #38

I still worry that having a typed Error (and typed throws in the future) will not be worth the programming complexity, and that additional error handling complexity opens the door for misunderstanding and misuse.

I'm still concerned that this is being proposed without other proposals as a driving factor at the language level (such as annotations on callbacks to generate Swift code that can use Result<T,E> rather than (T?, E?) signatures). Doing this now without a language driver seems to arbitrarily give up a chance at having the design be driven by an a language/compiler use case, rather than as a theoretical.

But given the ship has sailed, I'll think this looks like a good proposal for how Result should start.


(John McCall) #39

Named map, I think that would be somewhat confusing for people. If it weren't named map, it might be okay, but I think we'd want to wait before accepting it in the standard library just to see whether it's really generally useful. This won't be our last opportunity to add useful APIs to this type.


(John McCall) #40

Ah, I misunderstood the API when I was revising it. The new Result doesn't allow perfect typing of the result here, but I think the best translation might just be (Result<Data, Error>, URLResponse?).

EDIT: I've been informed that my original version was correct; receiving an Error is exclusive with receiving a URLResponse.