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


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

### Why this solution is better

There is one use case that this solution can handle properly that `rethrows` 
cannot.  This is because `rethrows` cannot see the implementation so it must 
assume that if any of the arguments throw the function itself can throw.  This 
is a consequence of not being able to see the implementation and not knowing 
whether the errors thrown from one of the functions might be handled 
internally.  It could be worked around with an additional argument annotation 
`@handled` or something similar, but that is getting clunky and adding special 
case features to the language.  It is much better to remove the special feature 
of `rethrows` and adopt a solution that can handle edge cases like this.

Here's an example that `rethrows` can't handle:

func takesTwo<E, F>(_ e: () throws(E) -> Void, _ f: () throws(F) -> Void) 
throws(E) -> Void {
  try e()
  do {
    try f()
  } catch _ {
    print("I'm swallowing f's error")
  }
}

// Should not require a `try` but does in the `rethrows` system.
takesTwo({}, { throw MyError() })

When this function is called and `e` does not throw, rethrows will still 
consider `takesTwo` a throwing function because one of its arguments throws.  
By considering all functions that throw an uninhabited type to be non-throwing, 
if `e` is non-throwing (has an uninhabited error type) then `takesTwo` is also 
non-throwing even if `f` throws on every invocation.  The error is handled 
internally and should not cause `takesTwo` to be a throwing function when 
called with these arguments.

## Error propegation

I used a generic function in the above example but the demonstration of the 
behavior of `rethrows` and how it requires manual error propegation when there 
is more than one unbounded error type involved if you want to preserve type 
information is all relevant in a non-generic context.  You can replace the 
generic error types in the above example with hard coded error types such as 
`enum TransformError: Error` and `enum AccumulateError: Error` in the above 
example and you will still have to write the exact same manual code to 
propegate the error.  This is the case any time the only common supertype is 
`Error`.

Before we go further, it's worth considering why propegating the type 
information is important.  The primary reason is that rethrowing functions do 
not introduce *new* error dependencies into calling code.  The errors that are 
thrown are not thrown by dependencies of the rethrowing function that we would 
rather keep hidden from callers.  In fact, the errors are not really thrown by 
the rethrowing function at all, they are only propegated.  They originate in a 
function that is specified by the caller and upon which the caller therefore 
already depends.  

In fact, unless the rethrowing function has unusual semantics the caller is 
likely to expect to be able catch any errors thrown by the arguments it 
provides in a typed fashion.  In order to allow this, a rethrowing function 
that takes more than one throwing argument must preserve error type information 
by injecting it into a sum type.  The only way to do this is to catch it and 
wrap it as can be seen in the example above.

### Factoring out some of the propegation boilerplate

There is a pattern we can follow to move the boilerplate out of our 
(re)throwing functions and share it between them were relevant.  This keeps the 
control flow in (re)throwing functions more managable while allowing us to 
convert errors during propegation.  This pattern involves adding an overload of 
a global name for each conversion we require:

func propegate<E, F, T>(@autoclosure f: () throws(E) -> T) 
      rethrows(TransformAndAccumulateError<E, F>) -> T  {
  do {
    try f()
  } catch let e {
    throw .transformError(e)
  }
}
func propegate<E, F, T>(@autoclosure f: () throws(F) -> T) 
      rethrows(TransformAndAccumulateError<E, F>) -> T  {
  do {
    try f()
  } catch let e {
    throw .accumulateError(e)
  }
}

Each of these overloads selects a different case based on the type of the error 
that `f` throws.  The way this works is by using return type inference which 
can see the error type the caller has specified.  The types used in these 
examples are intentionally domain specific, but `TransformAndAccumulateError` 
could be replaced with generic types like `Either` for cases when a rethrowing 
function is simply propegating errors provided by its arguments.

### Abstraction of the pattern is not possible

It is clear that there is a pattern here but unforuntately we are not able to 
abstract it in Swift as it exists today.

func propegate<E, F, T>(@autoclosure f: () throws(E) -> T) rethrows(F) -> T 
   where F: ??? initializable with E ??? {
   do {
      try f()
   } catch let e {
      throw // turn e into f somehow: F(e) ???
   }
}

### The pattern is still cumbersome

Even if we could abstract it, this mechanism of explicit propegation is still a 
bit cumbersome.  It clutters our code without adding any clarity.

for value in values {
  let transformed = try propegate(try transform(value))
  accumulator = try propegate(try accumulate(accumulator, transformed))
}

Instead of a single statement and `try` we have to use one statement per error 
propegation along with 4 `try` and 2 `propegate`.

For contrast, consider how much more concise the original version was:

for value in values {
  accumulator = try accumulate(accumulator, transform(value))
}

Decide for yourself which is easier to read.

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


## Appendix: Unions

If we had union types in Swift we could specify `rethrows(E | F)`, which in the 
case of two `Never` types is `Never | Never` which is simply `Never`.  We get 
rethrows (and implicit propegation by subtyping) for free.  Union types have 
been explicitly rejected for Swift with special emphasis placed on both generic 
code *and* error propegation.  

In the specific case of rethrowing implicit propegation to this common 
supertype and coalescing of a union of `Never` is very useful.  It would allow 
easy propegation, preservation of type information, and coalescing of many 
`Never`s into a single `Never` enabling the simple defintion of nonthrowing 
function as those specified to throw `Never` without *needing* to consider 
functions throwing other uninhabitable types as non-throwing (although that 
might still be a good idea).

Useful as they may be in this case where we are only propegating errors that 
the caller already depends on, the ease with which this enables preservation of 
type information encourages propegating excess type information about the 
errors of dependencies of a function that its callers *do not* already depend 
on.  This increases coupling in a way that should be considered very carefully. 
 Chris Lattner stated in the thread regarding this proposal that one of the 
reasons he opposes unions is because they make it too easy too introduce this 
kind of coupling carelessly.

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

Reply via email to