> On Feb 23, 2017, at 10:58 AM, Anton Zhilin <[email protected]> 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 
> <[email protected] <mailto:[email protected]>>:
> # 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
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to