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