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

Reply via email to