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) 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? 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 <antonyzhi...@gmail.com
<mailto:antonyzhi...@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@swift.org <mailto:swift-evolution@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@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to