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

IMO mapValues should actually be just called map. This is the operation that actually fulfills the functor laws for Dictionary. Unfortunately, the name Dictionary.map was already taken by Sequence.map which is misnamed IMO as it only fulfills the functor laws for arrays. It is the name of Sequence.map that is problematic if we want to have truly consistent naming for abstract operations.

9 Likes

I'd still prefer Result<T>, but I suppose this updated proposal works well enough (I was -1 on the original)

I really think 1st class interop between error handling and Result is important. To that end:

I use this functionality a lot in my own Result type.

A couple of name suggestions:

//For recovering a value from a given error (often similar to ?? for optionals)
func recover<U>(_ transform: (Error)throws -> U) -> Result<U,Swift.Error>

//For chaining throwing func together with result (similar to optional chaining)
func then<U>(_ transform: (Value)throws -> U) -> Result<U,Swift.Error>

As an example:

let myResult = Result{try myThrowingFunc()}.then(myNextThowingFunc).then(oneMore)

The cost of both of these is that they lose the typed error (because they need to be able to catch any thrown error)... but this should be fine for it's common use cases. If we ever get typed throws, then the result error type would match the thrown type (which is consistent with this version).

I imagine you could also make a function to assert that we think we know the error type (or otherwise narrow it):

func castError<E:Swift.Error>(to: E.type)throws -> Result<Value,E> //Needs a better name

and it would throw if we are wrong.

1 Like

This seems like a fair utilitarian argument to include extension Array : Error where Element : Error in the stdlib.

Is it?

This extension of the domain of errors does not quite scale to other useful protocols such as RecoverableError, LocalizedError, CustomNSError.

The scenario I foresee is the following:

  1. Yes! Let's throw arrays of errors! :tada:
  2. (Time passes... Localization topic enters the scene)
  3. Oh noes! My arrays of errors are quite uneasy to localize! I have to refactor my whole error stack around a CompoundError struct that wraps an array of errors! :sob:

The frustration described in this scenario is avoided when users start from a CompoundError struct right from the start.

Would it not make more sense to return an array of Results, each set to .error, instead of one Result with an array of Errors?

They are different things. Each are independently useful, but they are just useful in different situations.

Result<Value, [Error]> says I have multiple things that can go wrong in trying to obtain this one value.

Whereas [Result<Value, Error>] says I have multiple values that can fail.

If we try to model the former with the latter we'd have to worry about the situation where we get back multiple .value's when we know there should only be one value.

4 Likes

It depends on the purpose:

A sequence of Results lets you deal with errors without killing the whole sequence. Think of a stream of network packet, for example.

On the other side, a Result of sequence is more apt when, say, you need the content of a file, or all the rows from of a database engine.

And there is a third case, which happens for example when you lazily read the rows from a database. In this case, a sequence of Result is inapt because the first error should stop the sequence, and a Result of sequence is inapt because it can not be lazy.

Then we'd probably want the same for Dictionary and Set. And if performance ever becomes a concern then probably also ArraySlice and ContiguousArray. Also lazy collections of errors might be useful.

Maybe that's all reasonable, just feels like bending over backwards to support the error constraint on Result.

I understand the difference, but I wonder how often this scenario comes up. How often do you want to collect a group of errors that all relate to one failure? Is it not more common to bail on the first detected error?

I, too, would prefer if Result's Error was not constrained, but I don't see designing out this particular edge case to be problematic.

I'm not following. A Value which is a sequence is not a problem. The proposed design handles returning a result set of rows from a DB or a stream of Results for a series of network requests with equal facility. The question is about aggregating errors.

My apologies, @Avi, I was mislead.

You would want this anytime a thing could fail in multiple ways, which happens very often. A password can be invalid because it's too short and doesn't contain any special characters. A user couldn't be registered because their password is invalid and their email is invalid. The expression sqrt(a) + sqrt(b) can't be computed because a and b are negative.

And it's not just this one case. It's a part of a bigger problem of types that cannot conform to Error, like tuples, functions and all 3rd party types.

4 Likes

+1 from me!

This ship has *almost certainly* sailed on this but there is a way we could unify throws and Result into one error handling mechanism. This is our current state of throw:

func throwingFunction() throws -> String { // Implementation }

func consumingFunction() {
    do {
        let string = try throwingFunction()
    } catch {
        print(error)
    }
}

If try got sugar on a result type to mean "throw on Error or return the value", we'd get the same behavior in the consuming function:

func throwingFunction() -> Result<String, Error> { // Implementation }

func consumingFunction() {
    do {
        let string = try throwingFunction()
    } catch {
        print(error)
    }
}

Except we'd also get the ability to handle it without a do/catch if that's desired:

 func consumingFunction() {
     let result = throwingFunction() { // Sample syntax
        // Do something with the Result
     }
 }

Since throws and Result would be consumed the same way, you could have throws -> String be sugar for -> Result<String, Error> and run all error handling through result without a change in syntax.

EDIT:

I'd also add that when async comes around you could use the same technique for Promises, aka:

func asyncFunction() async -> String { // Implementation }

being equivalent to

func asyncFunction() -> Promise<String> { // Implementation }

Why is that novel to Result? Seems like you’d want your password-validating function to throw a MalformedPasswordError that has a list of reasons, not to introduce arbitrary multi-errors.

3 Likes

-1

The little this features adds after adding "async" and "await" we can leave for an opt-in package. Further more, using "Result" under new concurrency model would be fast deprecated and considered like bad practice. For special need anyone can either write it in few lines of code or use third party. Adding this to standard library will let the impression that "Result" should be used.
The swift language is clean and sensible and I feel like this is no place for "Result". Personally I do use "Result" and this approach help me to deal properly with callback async today, hopefully not tomorrow.

1 Like

MalformedPasswordError is still a wrapper type that will not behave like the type it wraps. I would rather use an array of errors so that I could then use that value as an array without needing to unwrap+transform+wrap it later.

I'm still +1 on this proposal. I just want to express that I think wrapper types can be really annoying to work with and lead to a bunch of types that are halfway implemented. It isn't really a solution to the underlying problem that the Error constraint significantly limits the type of values we can use for errors.

1 Like

Please keep in mind that we are being asked to review not the inclusion of Result, which is already accepted, but the proposed changes to the design.

4 Likes

I don't want to sound impatient, but can the core team clarify if we can retrofit the revised implementation after this last review into the Swift 5 branch, or is it too much of a hassle? Also since this proposal is coming hands in hands with the very first self-conforming protocol (Error) can you please officially clarify the ultimate long term goal we want to reach here?! I mean this is all great and so but we are waving through an orthogonal feature for one protocol without a full proposal, so do we now officially want to find a general solution for self-conformance for all protocols in the future?

Thank you very much in advance. :slightly_smiling_face:

3 Likes

Alright, understood.

The back & forth here about whether we should support Result<T, [Error]>, make Array : Error where Element : Error, or have users create their own AggregateError type makes me wonder if maybe we shouldn’t explore adding some sugar or language features to make wrapper types easier to create and work with in the first place (in a different thread). Just a thought.