[Discussion] Analysis of the design of typed throws


(Matthew Johnson) #1

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


Why doesn't Swift have explicit throwables like Java
(Matthew Johnson) #2

I put together some valid Swift 3 sample code in case anyone is having trouble understanding the discussion of rethrows. The behavior may not be immediately obvious.

func ithrow() throws { throw E.e }
func nothrow() {}

func rethrower(f: () throws -> Void, g: () throws -> Void) rethrows {
    do {
        try f()

        // I am not allowed to call `ithrow` here because it is not an argument
        // and a throwing catch clause is reachable if it throws.
        // This is because in a given invocation `f` might not throw but `ithrow` does.
        // Allowing the catch clause to throw an error in that circumstance violates the
        // invariant of `rethrows`.
        //
        // try ithrow()
    } catch _ as E {
        // I am allowed to catch an error if one is dynamically thrown by an argument.
        // At this point I am allowed to throw *any* error I wish.
        // The error I rethrow is not restricted in any way at all.
        // That *does not*
        throw F.f
    }
    do {
        // Here I am allowed to call `ithrow` because the error is handled.
        // There is no chance that `rethrower` throws evne if `ithrow` does.
        try ithrow()

        // We handle any error thrown by `g` internally and don't propegate it.
        // If `f` is a non-throwing function `rethrower` should be considered non-throwing
        // regardless of whether `g` can throw or not because if `g` throws the error is handled.
        // Unfortunately `rethrows` is not able to handle this use case.
        // We need to treat all functions with an uninhabitable errror type as non-throwing
        // if we want to cover this use case.
        try g()
    } catch _ {
        print("The error was handled internally")
    }
}

// `try` is obviously required here.
try rethrower(f: ithrow, g: ithrow)

// `try` is obviously not required here.
// This is the case `rethrows` can handle correctly: *all* the arguments are non-throwing.
rethrower(f: nothrow, g: nothrow)

// ok: `f` can throw so this call can as well.
try rethrower(f: ithrow, g: nothrow)

// I should be able to remove `try` here because any error thrown by `g` is handled internally
// by `rethrower` and is not propegated.
// If we treat all functions with an uninhabitable error type as non-throwing it becomes possible
// to handle this case when all we're doing is propegating errors that were thrown.
// This is because in this example we would only be propegating an error thrown by `f` and thus
// we would be have an uninhabitable error type.
// This is stil true if you add additional throwing arguments and propegate errors from
// several of them using a sum type.
// In that case we might have an error type such as Either<AnUninhabitableType, AnotherUninhabitableType>.
// Because all cases of the sum type have an associated value with an uninhabitable the sum type is as well.
try rethrower(f: nothrow, g: ithrow)

···

On Feb 22, 2017, at 6:37 PM, Matthew Johnson via swift-evolution <swift-evolution@swift.org> wrote:

# 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


(Anton Zhilin) #3

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>:


(David Hart) #4

Sending to mailing list:

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

What? This makes no sense to me. Can you elaborate? I don't see the relationship about rethrows and catch.

···

On 23 Feb 2017, at 01:37, Matthew Johnson via swift-evolution <swift-evolution@swift.org> wrote:

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


(Matthew Johnson) #5

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.

···

On Feb 23, 2017, at 10:58 AM, Anton Zhilin <antonyzhilin@gmail.com> wrote:

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


(Karl) #6

It’s funny, I literally just came across this. Turns out this is what the Dispatch overlay uses for dispatch_sync/DispatchQueue.sync.

Here’s an even shorter example:

func throwsUnexpected(one: ()throws->Void, hack: (Error)throws->Void) rethrows {
    try hack(SomeUnexpectedError.boo)
}

func hackedRethrow(func: ()throws->Void) rethrows {
    try throwsUnexpected(one: func, hack: { throw $0 })
}

The compiler allows this. Even though hackedRethrow says it rethrows the error from the closure, it calls in to another closure which, to its credit, does rethrow — albeit errors from the wrong closure!

It’s a handy hack, so if it was removed we’d need some way to instruct the compiler “even though you can’t prove it, I promise this function only ever rethrows errors from the closure”. There are legitimate use-cases for this (such as the aforementioned DispatchQueue.sync)

- Karl

···

On 23 Feb 2017, at 19:09, Matthew Johnson via swift-evolution <swift-evolution@swift.org> wrote:

I put together some valid Swift 3 sample code in case anyone is having trouble understanding the discussion of rethrows. The behavior may not be immediately obvious.

func ithrow() throws { throw E.e }
func nothrow() {}

func rethrower(f: () throws -> Void, g: () throws -> Void) rethrows {
   do {
       try f()

       // I am not allowed to call `ithrow` here because it is not an argument
       // and a throwing catch clause is reachable if it throws.
       // This is because in a given invocation `f` might not throw but `ithrow` does.
       // Allowing the catch clause to throw an error in that circumstance violates the
       // invariant of `rethrows`.
       //
       // try ithrow()
   } catch _ as E {
       // I am allowed to catch an error if one is dynamically thrown by an argument.
       // At this point I am allowed to throw *any* error I wish.
       // The error I rethrow is not restricted in any way at all.
       // That *does not*
       throw F.f
   }
   do {
       // Here I am allowed to call `ithrow` because the error is handled.
       // There is no chance that `rethrower` throws evne if `ithrow` does.
       try ithrow()

       // We handle any error thrown by `g` internally and don't propegate it.
       // If `f` is a non-throwing function `rethrower` should be considered non-throwing
       // regardless of whether `g` can throw or not because if `g` throws the error is handled.
       // Unfortunately `rethrows` is not able to handle this use case.
       // We need to treat all functions with an uninhabitable errror type as non-throwing
       // if we want to cover this use case.
       try g()
   } catch _ {
       print("The error was handled internally")
   }
}

// `try` is obviously required here.
try rethrower(f: ithrow, g: ithrow)

// `try` is obviously not required here.
// This is the case `rethrows` can handle correctly: *all* the arguments are non-throwing.
rethrower(f: nothrow, g: nothrow)

// ok: `f` can throw so this call can as well.
try rethrower(f: ithrow, g: nothrow)

// I should be able to remove `try` here because any error thrown by `g` is handled internally
// by `rethrower` and is not propegated.
// If we treat all functions with an uninhabitable error type as non-throwing it becomes possible
// to handle this case when all we're doing is propegating errors that were thrown.
// This is because in this example we would only be propegating an error thrown by `f` and thus
// we would be have an uninhabitable error type.
// This is stil true if you add additional throwing arguments and propegate errors from
// several of them using a sum type.
// In that case we might have an error type such as Either<AnUninhabitableType, AnotherUninhabitableType>.
// Because all cases of the sum type have an associated value with an uninhabitable the sum type is as well.
try rethrower(f: nothrow, g: ithrow)

On Feb 22, 2017, at 6:37 PM, Matthew Johnson via swift-evolution <swift-evolution@swift.org> wrote:

# 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

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


(Anton Zhilin) #7

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.

OK, if a function throws on itself (which is an unusual situation), it will
state its semantics in documentation, and it's the right place to do that.

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

Yes, I understood that.

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.

I'm positive about baking removal of `rethrows` into the proposal.
The specific example seems superficial to me. Usually we want to require
the bare minimum from the caller. But here we require a proper error type,
which is never used. Although, it may just be a convenience overload, and
the other overload accepts `() -> Bool` or `() -> Void?`.

···

2017-02-23 20:09 GMT+03:00 Matthew Johnson <matthew@anandabits.com>:

On Feb 23, 2017, at 10:58 AM, Anton Zhilin <antonyzhilin@gmail.com> wrote:


(Vladimir) #8

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 <antonyzhilin@gmail.com >> <mailto:antonyzhilin@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


(Matthew Johnson) #9

Sending to mailing list:

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

What? This makes no sense to me. Can you elaborate? I don't see the relationship about rethrows and catch.

This was a little bit surprising to me as well but I wouldn’t want it any other way. Here is some valid Swift 3 sample code demonstrating how this works:

enum E: Error { case e }
enum F: Error { case f }

func ithrow() throws { throw E.e }
func nothrow() {}

func rethrower(f: () throws -> Void, g: () throws -> Void) rethrows {
    do {
        try f()

        // I am not allowed to call `ithrow` here because it is not an argument
        // and a throwing catch clause is reachable if it throws.
        // This is because in a given invocation `f` might not throw but `ithrow` does.
        // Allowing the catch clause to throw an error in that circumstance violates the
        // invariant of `rethrows`.
        //
        // try ithrow()
    } catch _ as E {
        // I am allowed to catch an error if one is dynamically thrown by an argument.
        // At this point I am allowed to throw *any* error I wish.
        // The error I rethrow is not restricted in any way at all.
        // That *does not*
        throw F.f
    }
    do {
        // Here I am allowed to call `ithrow` because the error is handled.
        // There is no chance that `rethrower` throws evne if `ithrow` does.
        try ithrow()

        // We handle any error thrown by `g` internally and don't propegate it.
        // If `f` is a non-throwing function `rethrower` should be considered non-throwing
        // regardless of whether `g` can throw or not because if `g` throws the error is handled.
        // Unfortunately `rethrows` is not able to handle this use case.
        // We need to treat all functions with an uninhabitable errror type as non-throwing
        // if we want to cover this use case.
        try g()
    } catch _ {
        print("The error was handled internally")
    }
}

···

On Feb 24, 2017, at 12:06 PM, David Hart <david@hartbit.com> wrote:
On 23 Feb 2017, at 01:37, Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

## 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 <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Matthew Johnson) #10

2017-02-23 20:09 GMT+03:00 Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>>:

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.

OK, if a function throws on itself (which is an unusual situation), it will state its semantics in documentation, and it's the right place to do that.

I don’t understand what you mean here.

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

Yes, I understood that.

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.

I'm positive about baking removal of `rethrows` into the proposal.

Great!

The specific example seems superficial to me. Usually we want to require the bare minimum from the caller. But here we require a proper error type, which is never used. Although, it may just be a convenience overload, and the other overload accepts `() -> Bool` or `() -> Void?`.

I don’t understand what you mean here. In this alternative design *all* functions / closures / methods have an error type. If one is not stated explicitly it defaults to `Never`. If `throws` is specified without a type it defaults to `Error`. There is no burden at all placed on callers.

···

On Feb 23, 2017, at 11:41 AM, Anton Zhilin <antonyzhilin@gmail.com> wrote:

On Feb 23, 2017, at 10:58 AM, Anton Zhilin <antonyzhilin@gmail.com <mailto:antonyzhilin@gmail.com>> wrote:


(Matthew Johnson) #11

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)

Thrown errors already have an implicit type: `Error`. What this proposal does is allow us to provide more specific types.

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?

Yes, it handles this. When more than one concrete error type is possible you will need to specify a common supertype or wrap them in an enum. The suggested enhancement around implicit conversion during propagation will make this easier. Until then we will need to manually wrap the errors. I showed a pattern that can be used to do this with a reasonably small syntactic weight in functions that need to convert from one error type to another during propagation.

···

On Feb 23, 2017, at 11:53 AM, Vladimir.S <svabox@gmail.com> wrote:

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 <antonyzhilin@gmail.com >>> <mailto:antonyzhilin@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


(Matthew Johnson) #12

It’s funny, I literally just came across this. Turns out this is what the Dispatch overlay uses for dispatch_sync/DispatchQueue.sync.

Here’s an even shorter example:

func throwsUnexpected(one: ()throws->Void, hack: (Error)throws->Void) rethrows {
    try hack(SomeUnexpectedError.boo)
}

func hackedRethrow(func: ()throws->Void) rethrows {
    try throwsUnexpected(one: func, hack: { throw $0 })
}

The compiler allows this. Even though hackedRethrow says it rethrows the error from the closure, it calls in to another closure which, to its credit, does rethrow — albeit errors from the wrong closure!

It’s a handy hack, so if it was removed we’d need some way to instruct the compiler “even though you can’t prove it, I promise this function only ever rethrows errors from the closure”. There are legitimate use-cases for this (such as the aforementioned DispatchQueue.sync)

It turns out to be crucial if you want to be able to propagate error types from more than one throwing argument. `Either<E, F>` is not a subtype of `E` or `F`. The only way to propagate both errors without resorting to simply specifying `Error` as your error type is to catch them and wrap them (at least until we have a built-in mechanism for implicit error conversions).

···

On Feb 23, 2017, at 12:24 PM, Karl Wagner <razielim@gmail.com> wrote:

- Karl

On 23 Feb 2017, at 19:09, Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

I put together some valid Swift 3 sample code in case anyone is having trouble understanding the discussion of rethrows. The behavior may not be immediately obvious.

func ithrow() throws { throw E.e }
func nothrow() {}

func rethrower(f: () throws -> Void, g: () throws -> Void) rethrows {
   do {
       try f()

       // I am not allowed to call `ithrow` here because it is not an argument
       // and a throwing catch clause is reachable if it throws.
       // This is because in a given invocation `f` might not throw but `ithrow` does.
       // Allowing the catch clause to throw an error in that circumstance violates the
       // invariant of `rethrows`.
       //
       // try ithrow()
   } catch _ as E {
       // I am allowed to catch an error if one is dynamically thrown by an argument.
       // At this point I am allowed to throw *any* error I wish.
       // The error I rethrow is not restricted in any way at all.
       // That *does not*
       throw F.f
   }
   do {
       // Here I am allowed to call `ithrow` because the error is handled.
       // There is no chance that `rethrower` throws evne if `ithrow` does.
       try ithrow()

       // We handle any error thrown by `g` internally and don't propegate it.
       // If `f` is a non-throwing function `rethrower` should be considered non-throwing
       // regardless of whether `g` can throw or not because if `g` throws the error is handled.
       // Unfortunately `rethrows` is not able to handle this use case.
       // We need to treat all functions with an uninhabitable errror type as non-throwing
       // if we want to cover this use case.
       try g()
   } catch _ {
       print("The error was handled internally")
   }
}

// `try` is obviously required here.
try rethrower(f: ithrow, g: ithrow)

// `try` is obviously not required here.
// This is the case `rethrows` can handle correctly: *all* the arguments are non-throwing.
rethrower(f: nothrow, g: nothrow)

// ok: `f` can throw so this call can as well.
try rethrower(f: ithrow, g: nothrow)

// I should be able to remove `try` here because any error thrown by `g` is handled internally
// by `rethrower` and is not propegated.
// If we treat all functions with an uninhabitable error type as non-throwing it becomes possible
// to handle this case when all we're doing is propegating errors that were thrown.
// This is because in this example we would only be propegating an error thrown by `f` and thus
// we would be have an uninhabitable error type.
// This is stil true if you add additional throwing arguments and propegate errors from
// several of them using a sum type.
// In that case we might have an error type such as Either<AnUninhabitableType, AnotherUninhabitableType>.
// Because all cases of the sum type have an associated value with an uninhabitable the sum type is as well.
try rethrower(f: nothrow, g: ithrow)

On Feb 22, 2017, at 6:37 PM, Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

# 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 <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

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


(Anton Zhilin) #13

I don’t understand what you mean here.

I was a bit confused. Thanks to your clarification, I discovered that
`rethrows` functions currently can use `throw` expression, but only in
`catch` scope, which handles error handling for one of function parameters.
In my view, this language feature should be removed without replacement if
we remove `rethrows`. Instead, we should always be able to throw error type
stated in `throws(...)`.

I don’t understand what you mean here. In this alternative design *all*

functions / closures / methods have an error type. If one is not stated
explicitly it defaults to `Never`. If `throws` is specified without a type
it defaults to `Error`. There is no burden at all placed on callers.

I meant that in that specific case I would prefer returning an optional. If
error value is not meaningful--and that is the case with `f` function
parameter and corresponding `F` error type--then we are dealing with simple
domain errors, which are expressed with returning optionals instead of
throwing. I'll include this example anyway.

···

2017-02-23 21:01 GMT+03:00 Matthew Johnson <matthew@anandabits.com>:


(Vladimir) #14

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)

Thrown errors already have an implicit type: `Error`. What this
proposal does is allow us to provide more specific types.

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?

Yes, it handles this. When more than one concrete error type is
possible you will need to specify a common supertype or wrap them in an
enum. The suggested enhancement around implicit conversion during
propagation will make this easier. Until then we will need to manually
wrap the errors. I showed a pattern that can be used to do this with a
reasonably small syntactic weight in functions that need to convert from
one error type to another during propagation.

Thank you for the answer. Just trying to understand better.
Currently we can have this working code:

enum BarError: Error {case reason}

func bar(_ x: Int) throws {
  if x == 0 { throw BarError.reason }
}

enum FooError: Error {case reason}

func foo(_ x: Int) throws {
  try bar(x)
  
  if x == 1 { throw FooError.reason }
}

do
{
  try foo(0)
}
catch let e as FooError {
  print(e)
}

Will the proposal require that we define 'foo' exactly as

func foo(_ x: Int) throws(FooError,BarError) {...}

and if compiler will require that we'll check all possible errors(i.e. FooError and BarError or with one common handler for Error) ? I.e.

do
{
  try foo(0)
}
catch let e as FooError {
  print(e)
}
catch let e as BarError { // otherwise compilation error
  print(e)
}

or similar

do
{
  try foo(0)
}
catch let e as FooError {
  print(e)
}
catch { // have to process all other errors in common handler
  print(e)
}

I.e. will compiler force us to handle all possible exceptions(or at least 'default' with Error type) like it forces us to handle all cases in switch(or use 'default')?

And if 'old' syntax (i.e. foo() throws {...}) without specifying list of exception classes will be allowed?

···

On 23.02.2017 21:04, Matthew Johnson wrote:

On Feb 23, 2017, at 11:53 AM, Vladimir.S <svabox@gmail.com> wrote:

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 >>>> <antonyzhilin@gmail.com <mailto:antonyzhilin@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

.


(Vladimir) #15

Thank you for replies, Matthew. They were very helpful to understand the proposed solution.

···

On 23.02.2017 21:04, Matthew Johnson wrote:

On Feb 23, 2017, at 11:53 AM, Vladimir.S <svabox@gmail.com> wrote:

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)

Thrown errors already have an implicit type: `Error`. What this
proposal does is allow us to provide more specific types.

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?

Yes, it handles this. When more than one concrete error type is
possible you will need to specify a common supertype or wrap them in an
enum. The suggested enhancement around implicit conversion during
propagation will make this easier. Until then we will need to manually
wrap the errors. I showed a pattern that can be used to do this with a
reasonably small syntactic weight in functions that need to convert from
one error type to another during propagation.

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 >>>> <antonyzhilin@gmail.com <mailto:antonyzhilin@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

.


(Matthew Johnson) #16

2017-02-23 21:01 GMT+03:00 Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>>:
I don’t understand what you mean here.

I was a bit confused. Thanks to your clarification, I discovered that `rethrows` functions currently can use `throw` expression, but only in `catch` scope, which handles error handling for one of function parameters.
In my view, this language feature should be removed without replacement if we remove `rethrows`. Instead, we should always be able to throw error type stated in `throws(...)`.

I agree with this as long as all functions with an uninhabitable error type are treated as non-throwing.

I don’t understand what you mean here. In this alternative design *all* functions / closures / methods have an error type. If one is not stated explicitly it defaults to `Never`. If `throws` is specified without a type it defaults to `Error`. There is no burden at all placed on callers.

I meant that in that specific case I would prefer returning an optional. If error value is not meaningful--and that is the case with `f` function parameter and corresponding `F` error type--then we are dealing with simple domain errors, which are expressed with returning optionals instead of throwing. I'll include this example anyway.

You would still be able to return an Optional with no trouble. It would have an implicit error type of `Never` and a return type of `Optional`. This would be treated as a non-throwing function.

···

On Feb 23, 2017, at 12:30 PM, Anton Zhilin <antonyzhilin@gmail.com> wrote:


(Matthew Johnson) #17

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)

Thrown errors already have an implicit type: `Error`. What this
proposal does is allow us to provide more specific types.

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?

Yes, it handles this. When more than one concrete error type is
possible you will need to specify a common supertype or wrap them in an
enum. The suggested enhancement around implicit conversion during
propagation will make this easier. Until then we will need to manually
wrap the errors. I showed a pattern that can be used to do this with a
reasonably small syntactic weight in functions that need to convert from
one error type to another during propagation.

Thank you for the answer. Just trying to understand better.
Currently we can have this working code:

enum BarError: Error {case reason}

func bar(_ x: Int) throws {
  if x == 0 { throw BarError.reason }
}

enum FooError: Error {case reason}

func foo(_ x: Int) throws {
  try bar(x)
  
  if x == 1 { throw FooError.reason }
}

do
{
  try foo(0)
}
catch let e as FooError {
  print(e)
}

Will the proposal require that we define 'foo' exactly as

func foo(_ x: Int) throws(FooError,BarError) {…}

No, in fact this is prohibited. You are only allowed to state a single error type.

The good news is that the code you show above continues to be valid. Throwing functions have an implicit error type of `Error` if one is not stated explicitly.

If you want to expose the concrete types of the errors a function might throw you can do something like this:

enum EitherError<E: Error, F: Error>: Error {
   case left(E)
   case right(F)
}
func foo(_ x: Int) throws(EitherError<FooError,BarError>) {…}

Note: you should consider carefully what error type to expose. It is often not a good idea to directly expose the error type of your dependencies like this.

and if compiler will require that we'll check all possible errors(i.e. FooError and BarError or with one common handler for Error) ? I.e.

do
{
  try foo(0)
}
catch let e as FooError {
  print(e)
}
catch let e as BarError { // otherwise compilation error
  print(e)
}

or similar

do
{
  try foo(0)
}
catch let e as FooError {
  print(e)
}
catch { // have to process all other errors in common handler
  print(e)
}

I.e. will compiler force us to handle all possible exceptions(or at least 'default' with Error type) like it forces us to handle all cases in switch(or use 'default’)?

If you don’t specify an error type this will work exactly as it does today. If you do specify error types you just need to be sure to catch all types that may thrown in the `do` block.

And if 'old' syntax (i.e. foo() throws {...}) without specifying list of exception classes will be allowed?

Yes

···

On Feb 23, 2017, at 1:03 PM, Vladimir.S <svabox@gmail.com> wrote:
On 23.02.2017 21:04, Matthew Johnson wrote:

On Feb 23, 2017, at 11:53 AM, Vladimir.S <svabox@gmail.com> wrote:

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 >>>>> <antonyzhilin@gmail.com <mailto:antonyzhilin@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

.


Why doesn't Swift have explicit throwables like Java
(Matthew Johnson) #18

Thank you for replies, Matthew. They were very helpful to understand the proposed solution.

You’re welcome. Happy to help!

···

On Feb 23, 2017, at 3:31 PM, Vladimir.S <svabox@gmail.com> wrote:

On 23.02.2017 21:04, Matthew Johnson wrote:

On Feb 23, 2017, at 11:53 AM, Vladimir.S <svabox@gmail.com> wrote:

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)

Thrown errors already have an implicit type: `Error`. What this
proposal does is allow us to provide more specific types.

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?

Yes, it handles this. When more than one concrete error type is
possible you will need to specify a common supertype or wrap them in an
enum. The suggested enhancement around implicit conversion during
propagation will make this easier. Until then we will need to manually
wrap the errors. I showed a pattern that can be used to do this with a
reasonably small syntactic weight in functions that need to convert from
one error type to another during propagation.

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 >>>>> <antonyzhilin@gmail.com <mailto:antonyzhilin@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

.


(Anton Zhilin) #19

When I started changing the proposal, I noticed that examples with throws(E)
look uglier than with rethrows, because of required constraint of E: Error.
So I’d like to also discuss removal of Error constraint and its
consequences. Some people talked about Objective-C interop. What are those
issues?
More than that, it would then be logical to remove Error altogether and
move localizedDescription to LocalizedError.