[Discussion] Analysis of the design of typed throws

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.

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

ยทยทยท

2017-02-23 3:37 GMT+03:00 Matthew Johnson via swift-evolution < swift-evolution@swift.org>: