[swift-evolution] [Discussion] Analysis of the design of typed throws

Matthew Johnson matthew at anandabits.com
Thu Feb 23 13:11:25 CST 2017


> On Feb 23, 2017, at 1:03 PM, Vladimir.S <svabox at gmail.com> wrote:
> 
> On 23.02.2017 21:04, Matthew Johnson wrote:
>> 
>>> On Feb 23, 2017, at 11:53 AM, Vladimir.S <svabox at gmail.com> wrote:
>>> 
>>> I'm really sorry to interrupt your discussion, but could someone
>>> describe(or point to some article etc) in two words why we need added
>>> complexity of typed throws(in comparing to use documentation)
>> 
>> Thrown errors already have an implicit type: `Error`.  What this
>> proposal does is allow us to provide more specific types.
>> 
>>> and *if* the suggested solution will guarantee that some method can
>>> throw only explicitly defined type(s) of exception(s) including any
>>> re-thrown exception?
>> 
>> Yes, it handles this.  When more than one concrete error type is
>> possible you will need to specify a common supertype or wrap them in an
>> enum.  The suggested enhancement around implicit conversion during
>> propagation will make this easier.  Until then we will need to manually
>> wrap the errors.  I showed a pattern that can be used to do this with a
>> reasonably small syntactic weight in functions that need to convert from
>> one error type to another during propagation.
> 
> Thank you for the answer. Just trying to understand better.
> Currently we can have this working code:
> 
> enum BarError: Error {case reason}
> 
> func bar(_ x: Int) throws {
> 	if x == 0 { throw BarError.reason }
> }
> 
> enum FooError: Error {case reason}
> 
> func foo(_ x: Int) throws {
> 	try bar(x)
> 	
> 	if x == 1 { throw FooError.reason }
> }
> 
> do
> {
> 	try foo(0)
> }
> catch let e as FooError {
> 	print(e)
> }
> 
> Will the proposal require that we define 'foo' exactly as
> 
> func foo(_ x: Int) throws(FooError,BarError) {…}

No, in fact this is prohibited.  You are only allowed to state a single error type.

The good news is that the code you show above continues to be valid.  Throwing functions have an implicit error type of `Error` if one is not stated explicitly.

If you want to expose the concrete types of the errors a function might throw you can do something like this:

enum EitherError<E: Error, F: Error>: Error {
   case left(E)
   case right(F)
}
func foo(_ x: Int) throws(EitherError<FooError,BarError>) {…}

Note: you should consider carefully what error type to expose.  It is often not a good idea to directly expose the error type of your dependencies like this.

> 
> and if compiler will require that we'll check all possible errors(i.e. FooError and BarError or with one common handler for Error) ? I.e.
> 
> do
> {
> 	try foo(0)
> }
> catch let e as FooError {
> 	print(e)
> }
> catch let e as BarError { // otherwise compilation error
> 	print(e)
> }
> 
> or similar
> 
> do
> {
> 	try foo(0)
> }
> catch let e as FooError {
> 	print(e)
> }
> catch { // have to process all other errors in common handler
> 	print(e)
> }
> 
> I.e. will compiler force us to handle all possible exceptions(or at least 'default' with Error type) like it forces us to handle all cases in switch(or use 'default’)?

If you don’t specify an error type this will work exactly as it does today.  If you do specify error types you just need to be sure to catch all types that may thrown in the `do` block.

> 
> And if 'old' syntax (i.e. foo() throws {...}) without specifying list of exception classes will be allowed?

Yes

> 
>> 
>>> The thread is really long and I personally was not able to follow it
>>> from the beginning(so I believe the answer can be helpful for others
>>> like me). Thank you(really).
>>> 
>>> On 23.02.2017 20:09, Matthew Johnson via swift-evolution wrote:
>>>> 
>>>>> On Feb 23, 2017, at 10:58 AM, Anton Zhilin
>>>>> <antonyzhilin at gmail.com <mailto:antonyzhilin at gmail.com>> wrote:
>>>>> 
>>>>> See some inline response below. Also, have you seen the issue I
>>>>> posted in Proposal thread? There is a way to create an instance of
>>>>> "any" type.
>>>> 
>>>> Yes, I saw that.  There is no problem with that at all.  As I point
>>>> out in the analysis below, rethrowing functions are allowed to throw
>>>> any error they want.  They are only limited by *where* they may
>>>> throw.
>>>> 
>>>>> 
>>>>> 2017-02-23 3:37 GMT+03:00 Matthew Johnson via swift-evolution
>>>>> <swift-evolution at swift.org <mailto:swift-evolution at swift.org>>:
>>>>> 
>>>>> # Analysis of the design of typed throws
>>>>> 
>>>>> ## Problem
>>>>> 
>>>>> There is a problem with how the proposal specifies `rethrows` for
>>>>> functions that take more than one throwing function.  The
>>>>> proposal says that the rethrown type must be a common supertype of
>>>>> the type thrown by all of the functions it accepts.  This makes
>>>>> some intuitive sense because this is a necessary bound if the
>>>>> rethrowing function lets errors propegate automatically - the
>>>>> rethrown type must be a supertype of all of the automatically
>>>>> propegated errors.
>>>>> 
>>>>> This is not how `rethrows` actually works though.  `rethrows`
>>>>> currently allows throwing any error type you want, but only in a
>>>>> catch block that covers a call to an argument that actually does
>>>>> throw and *does not* cover a call to a throwing function that is
>>>>> not an argument.  The generalization of this to typed throws is
>>>>> that you can rethrow any type you want to, but only in a catch
>>>>> block that meets this rule.
>>>>> 
>>>>> 
>>>>> ## Example typed rethrow that should be valid and isn't with this
>>>>> proposal
>>>>> 
>>>>> This is a good thing, because for many error types `E` and `F`
>>>>> the only common supertype is `Error`.  In a non-generic function
>>>>> it would be possible to create a marker protocol and conform both
>>>>> types and specify that as a common supertype.  But in generic code
>>>>> this is not possible.  The only common supertype we know about is
>>>>> `Error`.  The ability to catch the generic errors and wrap them in
>>>>> a sum type is crucial.
>>>>> 
>>>>> I'm going to try to use a somewhat realistic example of a generic
>>>>> function that takes two throwing functions that needs to be valid
>>>>> (and is valid under a direct generalization of the current rules
>>>>> applied by `rethrows`).
>>>>> 
>>>>> enum TransformAndAccumulateError<E, F> { case transformError(E)
>>>>> case accumulateError(F) }
>>>>> 
>>>>> func transformAndAccumulate<E, F, T, U, V>( _ values: [T], _ seed:
>>>>> V, _ transform: T -> throws(E) U, _ accumulate: throws (V, U) ->
>>>>> V ) rethrows(TransformAndAccumulateError<E, F>) -> V { var
>>>>> accumulator = seed try { for value in values { accumulator = try
>>>>> accumulate(accumulator, transform(value)) } } catch let e as E {
>>>>> throw .transformError(e) } catch let f as F { throw
>>>>> .accumulateError(f) } return accumulator }
>>>>> 
>>>>> It doesn't matter to the caller that your error type is not a
>>>>> supertype of `E` and `F`.  All that matters is that the caller
>>>>> knows that you don't throw an error if the arguments don't throw
>>>>> (not only if the arguments *could* throw, but that one of the
>>>>> arguments actually *did* throw).  This is what rethrows specifies.
>>>>> The type that is thrown is unimportant and allowed to be anything
>>>>> the rethrowing function (`transformAndAccumulate` in this case)
>>>>> wishes.
>>>>> 
>>>>> 
>>>>> Yes, upcasting is only one way (besides others) to convert to a
>>>>> common error type. That's what I had in mind, but I'll state it
>>>>> more explicitly.
>>>> 
>>>> The important point is that if you include `rethrows` it should not
>>>> place any restrictions on the type that it throws when its arguments
>>>> throw.  All it does is prevent the function from throwing unless
>>>> there is a dynamic guarantee that one of the arguments did in fact
>>>> throw (which of course means if none of them can throw then the
>>>> rethrowing function cannot throw either).
>>>> 
>>>>> 
>>>>> 
>>>>> ## Eliminating rethrows
>>>>> 
>>>>> We have discussed eliminating `rethrows` in favor of saying that
>>>>> non-throwing functions have an implicit error type of `Never`.
>>>>> As you can see by the rules above, if the arguments provided have
>>>>> an error type of `Never` the catch blocks are unreachable so we
>>>>> know that the function does not throw.  Unfortunately a definition
>>>>> of nonthrowing functions as functions with an error type of
>>>>> `Never` turns out to be too narrow.
>>>>> 
>>>>> If you look at the previous example you will see that the only way
>>>>> to propegate error type information in a generic function that
>>>>> rethrows errors from two arguments with unconstrained error types
>>>>> is to catch the errors and wrap them with an enum.  Now imagine
>>>>> both arguments happen to be non-throwing (i.e. they throw
>>>>> `Never`).  When we wrap the two possible thrown values `Never` we
>>>>> get a type of `TransformAndAccumulateError<Never, Never>`.  This
>>>>> type is uninhabitable, but is quite obviously not `Never`.
>>>>> 
>>>>> In this proposal we need to specify what qualifies as a
>>>>> non-throwing function.  I think we should specifty this in the way
>>>>> that allows us to eliminate `rethrows` from the language.  In
>>>>> order to eliminate `rethrows` we need to say that any function
>>>>> throwing an error type that is uninhabitable is non-throwing.  I
>>>>> suggest making this change in the proposal.
>>>>> 
>>>>> If we specify that any function that throws an uninhabitable type
>>>>> is a non-throwing function then we don't need rethrows.
>>>>> Functions declared without `throws` still get the implicit error
>>>>> type of `Never` but other uninhabitable error types are also
>>>>> considered non-throwing.  This provides the same guarantee as
>>>>> `rethrows` does today: if a function simply propegates the errors
>>>>> of its arguments (implicitly or by manual wrapping) and all
>>>>> arguments have `Never` as their error type the function is able to
>>>>> preserve the uninhabitable nature of the wrapped errors and is
>>>>> therefore known to not throw.
>>>>> 
>>>>> 
>>>>> Yes, any empty type should be allowed instead of just `Never`.
>>>>> That's a general solution to the ploblem with `rethrows` and
>>>>> multiple throwing parameters.
>>>> 
>>>> It looks like you clipped out the section "Why this solution is
>>>> better” which showed how `rethrows` is not capable of correctly
>>>> typing a function as non-throwing if it dynamically handles all of
>>>> the errors thrown by its arguments.  What do you think of that?  In
>>>> my opinion, it makes a strong case for eliminating rethrows and
>>>> introducing the uninhabited type solution from the beginning.
>>>> 
>>>>> 
>>>>> ### Language support
>>>>> 
>>>>> This appears to be a problem in search of a language solution.
>>>>> We need a way to transform one error type into another error type
>>>>> when they do not have a common supertype without cluttering our
>>>>> code and writing boilerplate propegation functions.  Ideally all
>>>>> we would need to do is declare the appropriate converting
>>>>> initializers and everything would fall into place.
>>>>> 
>>>>> One major motivating reason for making error conversion more
>>>>> ergonomic is that we want to discourage users from simply
>>>>> propegating an error type thrown by a dependency.  We want to
>>>>> encourage careful consideration of the type that is exposed
>>>>> whether that be `Error` or something more specific.  If conversion
>>>>> is cumbersome many people who want to use typed errors will resort
>>>>> to just exposing the error type of the dependency.
>>>>> 
>>>>> The problem of converting one type to another unrelated type
>>>>> (i.e. without a supertype relationship) is a general one.  It
>>>>> would be nice if the syntactic solution was general such that it
>>>>> could be taken advantage of in other contexts should we ever have
>>>>> other uses for implicit non-supertype conversions.
>>>>> 
>>>>> The most immediate solution that comes to mind is to have a
>>>>> special initializer attribute `@implicit init(_ other: Other)`.  A
>>>>> type would provide one implicit initializer for each implicit
>>>>> conversion it supports.  We also allow enum cases to be declared
>>>>> `@implicit`.  This makes the propegation in the previous example
>>>>> as simple as adding the `@implicit ` attribute to the cases of our
>>>>> enum:
>>>>> 
>>>>> enum TransformAndAccumulateError<E, F> { @implicit case
>>>>> transformError(E) @implicit case accumulateError(F) }
>>>>> 
>>>>> It is important to note that these implicit conversions *would
>>>>> not* be in effect throughout the program.  They would only be used
>>>>> in very specific semantic contexts, the first of which would be
>>>>> error propegation.
>>>>> 
>>>>> An error propegation mechanism like this is additive to the
>>>>> original proposal so it could be introduced later.  However, if we
>>>>> believe that simply passing on the error type of a dependency is
>>>>> often an anti-pattern and it should be discouraged, it is a good
>>>>> idea to strongly consider introducing this feature along with the
>>>>> intial proposal.
>>>>> 
>>>>> 
>>>>> Will add to Future work section.
>>>> 
>>>> 
>>>> 
>>>> _______________________________________________ swift-evolution
>>>> mailing list swift-evolution at swift.org
>>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>>> 
>> 
>> .
>> 



More information about the swift-evolution mailing list