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

Vladimir.S svabox at gmail.com
Thu Feb 23 15:31:28 CST 2017

Thank you for replies, Matthew. They were very helpful to understand the 
proposed solution.

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