Adding Result to the Standard Library

Wouldn't this be more naturally presented as an enum?

enum FooError : Int, Error {
    case unknownProblem = 0
    case knownProblem = 42
}

func foo() throws FooError -> String {
    if .. {
        return "foo"
    } else {
        throw .knownProblem
    }
}

Large scale APIs are by definition public/open. I consider them to be ones that are intended to be adopted by broad and diverse audiences, which are often combined in unexpected ways, and/or ones that evolve to support new things. These sorts of things all should use untyped errors, because they don't have a closed set of specific errors that can be thrown.

In contrast, non-public APIs are fully knowable and updatable when implementation changes. In some places, there are specific and knowable failure modes, or error handling is used for things that are not failures in the typical sense (e.g. the parser example).

To reiterate, I believe that the documentation should continue to advocate for untyped errors, as they are the right thing to use outside of specific cases. That said, the specific cases are important, particularly as Swift grows out of being an iOS programming language into a general language that spans from systems to scripting.

-Chris

2 Likes

This syntax is unfortunate, looks like -> is related to the types throw. Probably should start a new thread for typed throws though.

I think the most likely scenario with typed throws is that people will use enums, and make those enums conform to Error. There is nothing in the design I'm suggesting that prevents that, but I see no benefit to making the design require it.

I don't think there is benefit to waiting until async/await (which I'm a huge fan of) happens. Two reasons:

  1. Result is really really useful when you don't have async/await: part of the argument against adding it was that its utility goes down when async/await happen.

  2. When we do have async/await, the result type you will want for that will be more heavy weight than what we're talking about, and (depending on lots of TBD design decisions) could impose additional constraints (e.g. it could have methods that require an await). This means that it should be a separate type IMO.

-Chris

I don't think that anyone is saying that throwing an Int is actually desirable, we were just saying that it would be possible. If there were some desire to protect users from theirselves (something which Swift does not typically do) then it would be feasible/reasonable to put in an artificial limitation to force explicit thrown types to enums.

-Chris

As a more practical example, I would point to Decodable - errors could come:

  • From the data source, e.g. an I/O error, issue with the layout of a bundle, etc.
  • From the decoder/archivers, e.g. data format corruption or error
  • From the decodable types, e.g. retrieved data does not represent a valid state

I would argue that this actually favors typed throws. As a consumer of an API, it is pretty frustrating to get back an arbitrary Error and hope that I just happen to handle the myriad of underlying causes of the error. Essentially, an untyped Error requires me to know the implementation details of the API I'm using. This is an encapsulation violation.

In the Decodable case, there would ideally be a DecodeableError enum that represents the three scenarios you outline above, with appropriate payloads for each case. Then as a consumer of the Decodeable API, I can switch on those three cases, provide whatever custom handling I want for the three cases, and then be done in the assurance that nothing else is going to get leaked out to me.

1 Like

Correct, the example was only provided to relate an unconstrained Result to the constraint currently required for throwable types.

I would vehemently be opposed to that. It is incredibly useful to throw structs, because structs can contain payload information that is common to all error cases, without requiring me to encode that common information into the associated values of the enum cases. For example, if you throw an error while parsing a string, you want to throw a struct that contains the Range<String.Index> where the error was found and the "kind" enum. If you restrict throwing to only enums, you end up forcing me to put a Range<String.Index> associated value on every enum case, which is ridiculous.

For a real-life example, here are the struct errors I throw when parsing strings in DDMathParser: https://github.com/davedelong/DDMathParser/blob/swift4/MathParser/MathParserErrors.swift

4 Likes

It is a point worth arguing; we are talking about 3+ components in this case composited together. Is it better to have the call site attempt to abstract away the errors of the sub components, when it itself does not necessarily understand the appropriate way to recover from said errors?
Or instead, to wrap the errors to propagate them up, such as hypothetically getting something of the form DecodingError.decoderIssue(JSONDecoderError.dataUnavailable(URLError.notFound(FileError.fileNotFound)))?
Or a third option, of having the composed components throw their own errors under the assumption that the caller will either need general error handling or specific error recovery, and it is more likely they will understand the heuristics of the specific errors?

Agreed.

I am now on-board with typed throws for the specific cases that you mentioned: System programming, C interop, and well defined internal error domains.

But I believe we should emphasize untyped throws as the default and generally recommended approach, unless the developer really knows what s/he is doing.

Also, I am hoping that Error gains the ability to avoid memory allocation when the actual error type is something like a simple enum that takes less than a machine word of storage.

3 Likes

Are we going to tackle this typed throws thing now? If that's the case I think we might as well shelve this thread and start a new discussion for typed throws, because this thread is for the Result type and I fear that digressing isn't helping this specific pitch.

But just to make sure I understand what's been going back and forth:

  1. It seems some people are going back and forth on whether Result should be a 1-generic type, 2-generic unconstrained type, 2-generic with an Error constraint on the error case. People arguing for the constraint seem to want to "tie" Result into Swift's error system which brings up the typed throws debate.
  2. There's a strong argument that adding Result can be done without tackling the typed throws debate. Result by itself is a fairly straightforward type, and trying to tie it with typed throwing is, as @Joe_Groff puts: "is tying a boat anchor to a glider." This type is already being used in many codebases today, so it's already a well established type.
  3. Likewise, there's been discussion on how this will tie in with a one-day async/actor model. But this runs into point 2 in that a async/actor model is years away.

One question that I saw @anandabits ask, but I didn't see a clear answer as to whether introducing an unconstrained error type will cause people using custom Result with constrained errors to have to migrate to the new standard lib Result. I believe the answer is no, because these custom Results will shadow the standard libs Result. Edit: I misunderstood the question. The issue being asked was what would users of Result<T, E: Error> do if we introduced a standard untyped Result.

Should we put the constrained Error discussion into the proposal and let that be discussed during a review? I believe there's enough good discussion to fill out an Alternatives considered for whichever side the proposal doesn't push for.

Literally in every Swift project I work with I immediately add the Either<A,B> type with two .map_ for the .left and .right cases (I actually call it Coproduct<A,B>, but it's the same) and I heavily use it everywhere in the codebase, not just for distinguishing the happy path from one that throws an error, and my colleagues do the same, even the android developers that use Kotlin: it's an extremely useful generic concept with loads of use cases.

I'd be super happy if it was added to the standard library, and I suspect many would be. Also, it would have a good educational impact on junior developers because Either<A,B> it's the quintessential algebraic data type.

I also am on the side of "typed throws", not in the sense that I would specify the error type in a throw expression - that would be inconvenient for many reasons -, but I would support specifying the error type in a Result value, simply because if I have an actual proper type to work with when handling error cases, I can simply .mapError into a more generic container (eventually AnyError if needed).

Having a type that can be extended and transformed, and values of that type that can be passed around functions, makes error handling more powerful without being less convenient. A typed Result (Either is better, in my opinion) could also be easily made interoperable with the current untyped throw, so the fact that the latter is untyped doesn't represent, to me, a particular obstacle to adding a new type to the standard library.

In general, for the future, I'd suggest to move towards providing more extendable types - and features in general - that can be accessed by developers, instead of keywords and annotations.

3 Likes

I too would like to include Either in the proposal as I find it very useful. In the worst case most people decide to +1 without Either and move it to its own evolution proposal.

I might be misunderstanding what you mean by better. While Result in some forms is the same in that it's a 2-generic enum, the types differ in that they have different semantic information.

Either is a generic sum type with two cases used to represent a value that can have two discrete cases, with the cases carrying different semantic information depending if it's left or right.

Result is the same in this regards except it has semantics for conveying a failure case and a success case. Which is more refined than the generic Either type.

I meant better in the sense of if we're including one XOR the other, I prefer Either. Having both would be even better: Result for more powerful error handling, and Either as the generic sum type.

EDIT: I'm aware that Either is frequently used in the sense of a Result, where the .right case is the successful one (ex. in Haskell the Functor implementation for Either favors the .right case). I'd definitely prefer a more generic, unopinionated and bifunctorial Either, with both .mapLeft and .mapRight, and a Result with error/success cases.

I think in the context of what people want out of this discussion, Either is the wrong one to choose IMO.

People more often are using Result to propagate failures than they are modeling functions that can return two distinct types (which is the primary use case of Either).

And while Either is the more generic one, by itself it doesn't do a good job conveying the semantics most people are using this type of type for. So it fails my test for which one should be the primary inclusion into the standard lib.

Right, this is a product of Haskell using Either for error propagation, which a lot of people believe (and I tend to agree) was a mistake. Which is one reason why Rust went with the Result type.

I agree! That's why I would also favor including Either in the proposal, or just starting a whole new proposal for it.

@Jon_Shier Can you update your OP to provide the correct link to the proposal? (Preferably pointing to a commit so it doesn't break in the future)

No I can't, as I can no longer edit that post.

1 Like

This isn’t quite correct. The migration-related issue I brought up was what users of Result<T, E: Error> would do if we introduced a standard untyped Result. How many of these users would be willing to take on the migration work while also giving up typed errors? If we introduce Result<T, E> this issue won’t need to be addressed.

Ah, I misunderstood your question, thanks for clarifying.

Either is a completely separate matter. Please start a new thread if you'd like to discuss inclusion of it, and also check out previous discussions about it.

7 Likes