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

Matthew Johnson matthew at anandabits.com
Thu Feb 23 15:33:02 CST 2017


> On Feb 23, 2017, at 3:31 PM, Vladimir.S <svabox at gmail.com> wrote:
> 
> Thank you for replies, Matthew. They were very helpful to understand the proposed solution.

You’re welcome.  Happy to help!

> 
> 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.
>> 
>>> 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