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

Anton Zhilin antonyzhilin at gmail.com
Thu Feb 23 10:58:41 CST 2017


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.

2017-02-23 3:37 GMT+03:00 Matthew Johnson via swift-evolution <
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.


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

### 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.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170223/f28f655d/attachment.html>


More information about the swift-evolution mailing list