Hi David,

I spent a lot of time last night thinking about how my concerns can be 
addressed without changes to the type system.  I also spent some time working 
through some concrete examples this morning.  This has helped narrow my 
concerns considerably.

I am going to suggest one addition to the proposal at the end.  If you’re 
willing to incorporate that I will be pretty happy with what we can accomplish 
without any changes to the type system.

First, consider the case where there are some common errors which a library may 
throw in different places.  These are considered to be part of the API 
contract.  Some library functions may throw either common error depending on 
the code path taken.  

Your proposal suggests we should fall back to throwing ErrorType in that case.  
This is not really a good solution in my mind.  

A library should be able to have a family of error types it publishes in its 
API contract and some functions should be able to throw more than one.  As you 
suggest, rather than a structural sum type we can manually create a sum type to 
do this.  

I had two concerns about this.  The first and most important was in the 
verbosity of catching the nested errors.  Here’s an example:

enum CommonOne: ErrorType {
    case One
    case Two
}
enum CommonTwo:ErrorType {
    case One
    case Two
}
enum Both: ErrorType {
    case One(CommonOne)
    case Two(CommonTwo)
}

I was concerned that we would need to do something like this involving some 
verbose and nasty nesting, etc:

func functionThatThrowsBoth() throws Both { … }

do {
    try functionThatThrowsBoth()
}
catch .One(let inner) {
    switch inner {
        case .one: ...
        case .two: ...
    }
}
catch .Two(let inner) {
    switch inner {
        case .one: ...
        case .two: ...
    }
}

As it turns out, I am still getting familiar with the power of nested pattern 
matching and this was an unfounded concern.  This is great!  We can actually do 
this:

do {
    try functionThatThrowsBoth()
}
catch .One(.One) { ... }
catch .One(.Two) { ... }
catch .Two(.One) { ... }
catch .Two(.Two) { ... }

(note: this works today if you include a Both prefix in the cases which will be 
unnecessary with a typed error)

That is great!  I have no concerns about this syntax for catching nested 
errors.  This covers use cases that need to throw “multiple” error types pretty 
well.  There are probably some edge cases where a structural sum type would be 
more convenient but I think they would be rare and am not concerned about them.

I would also like to comment that there are some interesting related ideas for 
enhancing enums in the "[Pitch] Use enums as enum underlying types” thread.  
They don’t directly impact the proposal but could make such use cases even more 
convenient if they are pursued independently.

The other concern I have is still valid, but I think a relatively 
straightforward solution is possible.

Continuing with the previous example, let’s look at the implementation of 
`functionThatThrowsBoth`:

func throwsInnerOne() throws InnerOne {
    throw InnerOne.One
}

func throwsInnerTwo() throws InnerTwo {
    throw InnerTwo.Two
}

func functionThatThrowsBoth(_ whichError: Bool) throws Both {
    do {
        if whichError {
            try throwsInnerOne()
        } else {
            try throwsInnerTwo()
        }
    }
    catch let inner as InnerOne { throw Both.One(inner) }
    catch let inner as InnerTwo { throw Both.Two(inner) }
}

The implementation is dominated by the concern of wrapping the error.  This is 
pretty gross.  This problem exists even if we are not wrapping the error, but 
rather translating the error from the underlying error type into the error type 
we are publishing in our API contract.  

Here is an example where we are not wrapping the error, but translating it:

func functionThatCallsUnderlingyingThrows(_ whichError: Bool) throws 
MyPublishedError {
    do {
        try funcThatThrowsAnErrorThatMustBeTranslatedIntoMyPublishedError()
    }
    // catching logic that eventually throws MyPublishedErrorSomehow
}

The best we can do is to create a translation function or initializer:

enum MyPublishedError: ErrorType {
    init(_ error: UnderlyingError) { … }
}

func functionThatCallsUnderlingyingThrows(_ whichError: Bool) throws 
MyPublishedError {
    do {
        try funcThatThrowsAnErrorThatMustBeTranslatedIntoMyPublishedError()
    }
    catch let error as UnderlyingError { throw MyPublishedError(error) }
}

This is better as it removes the logic from the function itself.  But it’s 
still not great as it introduces a lot of boilerplate everywhere we need to 
translate and / or wrap errors.  The boilerplate also grows for each underlying 
error we need to translate:

func functionThatCallsUnderlingyingThrows(_ whichError: Bool) throws 
MyPublishedError {
    do {
        // bunch of stuff throwing several different errors
    }
    catch let error as UnderlyingError { throw MyPublishedError(error) }
    catch let error as OtherUnderlyingError { throw MyPublishedError(error) }
    // more catch clauses until we have covered every possible error type 
thrown by the body
    // hopefully the compiler wouldn’t require a default clause here but it 
probably would
}

This is the problem that `From` addresses in Rust.  Swift is not Rust and our 
solution will look different.  The point is that this is a problem and it can 
and has been solved.

My suggestion is that we should allow implicit conversion during error 
propagation.  If the published error type has one and only one non-failable, 
non-throwing initializer that takes a single argument of the type that is 
thrown (including enum case initializers with a single associated value of the 
thrown type) that initializer is used to implicitly convert to the published 
error type.  This conversion could be accomplished by synthesizing the 
necessary boilerplate or by some other means.

Now we have:

func functionThatCallsUnderlingyingThrows(_ whichError: Bool) throws 
MyPublishedError {
        try funcThatThrowsAnErrorThatMustBeTranslatedIntoMyPublishedError()
}

This looks as it should.  We don’t pay a price of boilerplate for carefully 
designing the errors we expose in our API contract.  This also handles 
automatic wrapping of errors where that is appropriate.

I don’t suggest implicit conversion lightly.  I generally hate implicit 
conversions.  But I think it makes a lot of sense here.  It keeps concerns 
separate and removes boilerplate that distracts from the logic at hand, thus 
vastly improving readability.  It is also likely to help minimize code impact 
when implementation details change and we need to modify how we are translating 
errors into the contract we expose.

If we don’t support the implicit conversion there are three paths that can be 
taken by developers.  None of them are great and we will have three camps with 
different preference:

1. Just stick to untyped errors.  I think there are some pretty smart people 
who think this will be a common practice even if we have support for typed 
errors in the language.
2. Allow underlying errors to flow through (when there is only a single 
underlying error type).  This is brittle and I know you are of the opinion that 
it is a bad idea.  I agree.
3. Write the boilerplate manually.  This is annoying and is a significant 
barrier to clarity and readability.

I hope you will like the idea of implicit conversion during error propagation 
enough to add it to your proposal.  With it I will be an enthusiastic 
supporter.  It will help to establish good practices in the community for using 
typed errors in a robust and thoughtful way.

Without implicit error conversion I will still support the proposal but would 
plan to write a follow on proposal introducing the much needed (IMO) implicit 
conversion during error propagation.  I would also expect opposition to the 
proposal during review from people concerned about one or more of the above 
listed options for dealing with error translation.

I think the idea of restricting typed errors to structs, enums, NSError, and 
final classes that has come up is a good one.  It might be worth considering 
going further than that and restrict it to only enums and NSError.  One of the 
biggest issues I have encountered with error handling during my career is that 
all too often the possible error cases are quite poorly documented.  We have to 
allow NSError for Cocoa interop, but aside from the error types should really 
be enums IMO as they make it very clear what cases might need to be handled.

I want to thank you again for putting this proposal together and taking the 
time to consider and respond to feedback.  Typed errors will be a significant 
step forward for Swift and I am looking forward to it.  

Matthew





> On Dec 18, 2015, at 12:36 PM, David Owens II <[email protected]> wrote:
> 
> 
>> On Dec 18, 2015, at 9:41 AM, Matthew Johnson <[email protected] 
>> <mailto:[email protected]>> wrote:
>> 
>> I’m not asking for you to speak for them.  But I do think we need to learn 
>> from communities that are having success with typed error handling.  Your 
>> proposal would be stronger if it went into detail about how it would avoid 
>> the problems that have been encountered in other languages.  The experience 
>> of Rust could help to make that case as it is concrete and not hypothetical.
> 
> Sure, it could. It’s also anecdotal. It’s not necessarily true that something 
> that works well in one context works well in another. It’s good to note that 
> typed errors are wholly considered bad, but I’m not sure how much further we 
> need to go then that. If you have specifics, then I could probably add them 
> as an addendum to the proposal.
> 
>> My understanding is that Rust uses static multi-dispatch to do this.  I 
>> don’t believe it has anything to do with structural sum types.  Rust error 
>> handling uses a Result type with a single error case: 
>> http://doc.rust-lang.org/book/error-handling.html 
>> <http://doc.rust-lang.org/book/error-handling.html>.
> 
> That example takes you through many of the options available. In the end, you 
> end up at the sum-type for the error:
> fn search<P: AsRef<Path>>
>          (file_path: &Option<P>, city: &str)
>          -> Result<Vec<PopulationCount>, CliError> {
>     ...
> }
> It’s the CliError which is defined as:
> enum CliError {
>     Io(io::Error),
>     Csv(csv::Error),
>     NotFound,
> }
> The From() function essentially allows the try! macro to expand these in a 
> nicer way.
> 
> So back to the proposal, one of the key things is to promote the `error` 
> constant throughout the catch-clauses. This means that we can already 
> leverage Swift’s pattern matching to solve this problem:
> 
> enum Combined {
>     case IO(String)
>     case Number(Int)
> }
> 
> func simulate(err: Combined) {
>     switch err {
>     case let Combined.IO(string) where string == "hi": print("only hi!")
>     case let Combined.IO(string): print(string)
>     case let Combined.Number(value): print(value)
>     }
> }
> 
> simulate(Combined.IO("hi"))
> simulate(Combined.IO("io"))
> simulate(Combined.Number(9))
> 
> It’s not hard to use Swift’s pattern matching to extract out the inner 
> information on an associated value enum and white the case/catch clauses. So 
> unless I’m missing something, I think Swift already provides a good mechanism 
> to do what you’re asking for, with the caveat that the `error` constant is 
> promoted to be usable in the catch-clauses similar to how the 
> switch-statements work.
> 
> Maybe adding this to the proposal would clarify usage?
> 
>> How does this create a fragile API surface area?  Adding a new error type to 
>> the signature would be a breaking change to the API contract.  This is 
>> really no different than changing the type of error that can be thrown under 
>> your proposal.
> 
> It’s the same fragility that enums create; this was covered in the criticisms 
> section. The likelihood of adding additional error cases is much greater than 
> a change that would completely change the type of the error.
> 
>> 
>>> I see this functionality as a general limitation in the language. For 
>>> example, errors are not the only context where you may want to return a 
>>> type of A, B, or C. There have been other proposals on how we might do that 
>>> in Swift. If and when it was solved in the general case for type 
>>> parameters, I can’t foresee a compelling reason why it wouldn’t work in 
>>> this context as well.
>> 
>> That makes sense in some ways, but I don’t think it’s unreasonable to ask 
>> for some analysis of whether a better design for typed errors would be 
>> possible if we had them.  IMO it’s pretty important to get the design of 
>> typed errors right if / when we add them.  If we don’t it will be considered 
>> a major mistake and will lead to a lot of less than desirable outcomes down 
>> the road.
>> 
>> I also think typed errors may be one of the more important use cases for 
>> structural sum types of some kind.  If we are able to show that design 
>> problems that cannot be solved without them can be solved with them that 
>> might influence whether they are added or not.  It might also influence when 
>> it makes sense to add support for typed errors to the language.
> 
> The problem can be solved without implicitly generated sum types though. The 
> design of typed errors, as proposed, is to be consistent with the Swift type 
> system today. Regardless, I’ve added a response in the “cirticisms” section 
> that hopefully addresses this in some manner - basically, yes it would be 
> helpful, but out of scope for this proposal.
> 
>> That approach would make catch clauses rather clunky by nesting errors 
>> inside of associated values.  If you’re advocating for this approach do you 
>> have any ideas on how to streamline syntax for catching them?
> 
> See above example. Does that address this concern?
> 
> -David
> 

_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to