<html><head><meta http-equiv="Content-Type" content="text/html charset=utf-8"></head><body style="word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space;" class=""><br class=""><div><blockquote type="cite" class=""><div class="">On Feb 24, 2017, at 12:06 PM, David Hart <<a href="mailto:david@hartbit.com" class="">david@hartbit.com</a>> wrote:</div><br class="Apple-interchange-newline"><div class=""><meta http-equiv="content-type" content="text/html; charset=utf-8" class=""><div dir="auto" class=""><div class="">Sending to mailing list:</div><div class=""><br class="">On 23 Feb 2017, at 01:37, Matthew Johnson via swift-evolution <<a href="mailto:swift-evolution@swift.org" class="">swift-evolution@swift.org</a>> wrote:<br class=""><br class=""></div><blockquote type="cite" class=""><div class=""><span class=""># Analysis of the design of typed throws</span><br class=""><span class=""></span><br class=""><span class="">## Problem</span><br class=""><span class=""></span><br class=""><span class="">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.</span><br class=""><span class=""></span><br class=""><span class="">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.</span><br class=""></div></blockquote><div class=""><br class=""></div><div class=""><span style="background-color: rgba(255, 255, 255, 0);" class="">What? This makes no sense to me. Can you elaborate? I don't see the relationship about <b class="">rethrows</b> and <b class="">catch</b>.</span></div></div></div></blockquote><div><br class=""></div><div>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:</div><div><br class=""></div><div><br class="">enum E: Error { case e }<br class="">enum F: Error { case f }<br class=""><br class="">func ithrow() throws { throw E.e }<br class="">func nothrow() {}<br class=""><br class="">func rethrower(f: () throws -> Void, g: () throws -> Void) rethrows {<br class=""> do {<br class=""> try f()<br class=""><br class=""> // I am not allowed to call `ithrow` here because it is not an argument<br class=""> // and a throwing catch clause is reachable if it throws.<br class=""> // This is because in a given invocation `f` might not throw but `ithrow` does.<br class=""> // Allowing the catch clause to throw an error in that circumstance violates the<br class=""> // invariant of `rethrows`.<br class=""> //<br class=""> // try ithrow()<br class=""> } catch _ as E {<br class=""> // I am allowed to catch an error if one is dynamically thrown by an argument.<br class=""> // At this point I am allowed to throw *any* error I wish.<br class=""> // The error I rethrow is not restricted in any way at all.<br class=""> // That *does not*<br class=""> throw F.f<br class=""> }<br class=""> do {<br class=""> // Here I am allowed to call `ithrow` because the error is handled.<br class=""> // There is no chance that `rethrower` throws evne if `ithrow` does.<br class=""> try ithrow()<br class=""><br class=""> // We handle any error thrown by `g` internally and don't propegate it.<br class=""> // If `f` is a non-throwing function `rethrower` should be considered non-throwing<br class=""> // regardless of whether `g` can throw or not because if `g` throws the error is handled.<br class=""> // Unfortunately `rethrows` is not able to handle this use case.<br class=""> // We need to treat all functions with an uninhabitable errror type as non-throwing<br class=""> // if we want to cover this use case.<br class=""> try g()<br class=""> } catch _ {<br class=""> print("The error was handled internally")<br class=""> }<br class="">}<br class=""><br class=""></div><br class=""><blockquote type="cite" class=""><div class=""><div dir="auto" class=""><br class=""><blockquote type="cite" class=""><div class=""><span class="">## Example typed rethrow that should be valid and isn't with this proposal</span><br class=""><span class=""></span><br class=""><span class="">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.</span><br class=""><span class=""></span><br class=""><span class="">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`).</span><br class=""><span class=""></span><br class=""><span class="">enum TransformAndAccumulateError<E, F> {</span><br class=""><span class=""> case transformError(E)</span><br class=""><span class=""> case accumulateError(F)</span><br class=""><span class="">}</span><br class=""><span class=""></span><br class=""><span class="">func transformAndAccumulate<E, F, T, U, V>(</span><br class=""><span class=""> _ values: [T], </span><br class=""><span class=""> _ seed: V,</span><br class=""><span class=""> _ transform: T -> throws(E) U, </span><br class=""><span class=""> _ accumulate: throws (V, U) -> V</span><br class=""><span class="">) rethrows(TransformAndAccumulateError<E, F>) -> V {</span><br class=""><span class=""> var accumulator = seed</span><br class=""><span class=""> try {</span><br class=""><span class=""> for value in values {</span><br class=""><span class=""> accumulator = try accumulate(accumulator, transform(value))</span><br class=""><span class=""> }</span><br class=""><span class=""> } catch let e as E {</span><br class=""><span class=""> throw .transformError(e)</span><br class=""><span class=""> } catch let f as F {</span><br class=""><span class=""> throw .accumulateError(f)</span><br class=""><span class=""> }</span><br class=""><span class=""> return accumulator</span><br class=""><span class="">}</span><br class=""><span class=""></span><br class=""><span class="">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.</span><br class=""><span class=""></span><br class=""><span class=""></span><br class=""><span class="">## Eliminating rethrows</span><br class=""><span class=""></span><br class=""><span class="">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.</span><br class=""><span class=""></span><br class=""><span class="">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`. </span><br class=""><span class=""></span><br class=""><span class="">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.</span><br class=""><span class=""></span><br class=""><span class="">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.</span><br class=""><span class=""></span><br class=""><span class="">### Why this solution is better</span><br class=""><span class=""></span><br class=""><span class="">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.</span><br class=""><span class=""></span><br class=""><span class="">Here's an example that `rethrows` can't handle:</span><br class=""><span class=""></span><br class=""><span class="">func takesTwo<E, F>(_ e: () throws(E) -> Void, _ f: () throws(F) -> Void) throws(E) -> Void {</span><br class=""><span class=""> try e()</span><br class=""><span class=""> do {</span><br class=""><span class=""> try f()</span><br class=""><span class=""> } catch _ {</span><br class=""><span class=""> print("I'm swallowing f's error")</span><br class=""><span class=""> }</span><br class=""><span class="">}</span><br class=""><span class=""></span><br class=""><span class="">// Should not require a `try` but does in the `rethrows` system.</span><br class=""><span class="">takesTwo({}, { throw MyError() })</span><br class=""><span class=""></span><br class=""><span class="">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.</span><br class=""><span class=""></span><br class=""><span class="">## Error propegation</span><br class=""><span class=""></span><br class=""><span class="">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`.</span><br class=""><span class=""></span><br class=""><span class="">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. </span><br class=""><span class=""></span><br class=""><span class="">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.</span><br class=""><span class=""></span><br class=""><span class="">### Factoring out some of the propegation boilerplate</span><br class=""><span class=""></span><br class=""><span class="">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:</span><br class=""><span class=""></span><br class=""><span class="">func propegate<E, F, T>(@autoclosure f: () throws(E) -> T) </span><br class=""><span class=""> rethrows(TransformAndAccumulateError<E, F>) -> T {</span><br class=""><span class=""> do {</span><br class=""><span class=""> try f()</span><br class=""><span class=""> } catch let e {</span><br class=""><span class=""> throw .transformError(e)</span><br class=""><span class=""> }</span><br class=""><span class="">}</span><br class=""><span class="">func propegate<E, F, T>(@autoclosure f: () throws(F) -> T) </span><br class=""><span class=""> rethrows(TransformAndAccumulateError<E, F>) -> T {</span><br class=""><span class=""> do {</span><br class=""><span class=""> try f()</span><br class=""><span class=""> } catch let e {</span><br class=""><span class=""> throw .accumulateError(e)</span><br class=""><span class=""> }</span><br class=""><span class="">}</span><br class=""><span class=""></span><br class=""><span class="">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.</span><br class=""><span class=""></span><br class=""><span class="">### Abstraction of the pattern is not possible</span><br class=""><span class=""></span><br class=""><span class="">It is clear that there is a pattern here but unforuntately we are not able to abstract it in Swift as it exists today.</span><br class=""><span class=""></span><br class=""><span class="">func propegate<E, F, T>(@autoclosure f: () throws(E) -> T) rethrows(F) -> T </span><br class=""><span class=""> where F: ??? initializable with E ??? {</span><br class=""><span class=""> do {</span><br class=""><span class=""> try f()</span><br class=""><span class=""> } catch let e {</span><br class=""><span class=""> throw // turn e into f somehow: F(e) ???</span><br class=""><span class=""> }</span><br class=""><span class="">}</span><br class=""><span class=""></span><br class=""><span class="">### The pattern is still cumbersome</span><br class=""><span class=""></span><br class=""><span class="">Even if we could abstract it, this mechanism of explicit propegation is still a bit cumbersome. It clutters our code without adding any clarity.</span><br class=""><span class=""></span><br class=""><span class="">for value in values {</span><br class=""><span class=""> let transformed = try propegate(try transform(value))</span><br class=""><span class=""> accumulator = try propegate(try accumulate(accumulator, transformed))</span><br class=""><span class="">}</span><br class=""><span class=""></span><br class=""><span class="">Instead of a single statement and `try` we have to use one statement per error propegation along with 4 `try` and 2 `propegate`.</span><br class=""><span class=""></span><br class=""><span class="">For contrast, consider how much more concise the original version was:</span><br class=""><span class=""></span><br class=""><span class="">for value in values {</span><br class=""><span class=""> accumulator = try accumulate(accumulator, transform(value))</span><br class=""><span class="">}</span><br class=""><span class=""></span><br class=""><span class="">Decide for yourself which is easier to read.</span><br class=""><span class=""></span><br class=""><span class="">### Language support</span><br class=""><span class=""></span><br class=""><span class="">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.</span><br class=""><span class=""></span><br class=""><span class="">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.</span><br class=""><span class=""></span><br class=""><span class="">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.</span><br class=""><span class=""></span><br class=""><span class="">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:</span><br class=""><span class=""></span><br class=""><span class="">enum TransformAndAccumulateError<E, F> {</span><br class=""><span class=""> @implicit case transformError(E)</span><br class=""><span class=""> @implicit case accumulateError(F)</span><br class=""><span class="">}</span><br class=""><span class=""></span><br class=""><span class="">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.</span><br class=""><span class=""></span><br class=""><span class="">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.</span><br class=""><span class=""></span><br class=""><span class=""></span><br class=""><span class="">## Appendix: Unions</span><br class=""><span class=""></span><br class=""><span class="">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. </span><br class=""><span class=""></span><br class=""><span class="">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).</span><br class=""><span class=""></span><br class=""><span class="">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.</span><br class=""><span class=""></span><br class=""><span class="">_______________________________________________</span><br class=""><span class="">swift-evolution mailing list</span><br class=""><span class=""><a href="mailto:swift-evolution@swift.org" class="">swift-evolution@swift.org</a></span><br class=""><span class=""><a href="https://lists.swift.org/mailman/listinfo/swift-evolution" class="">https://lists.swift.org/mailman/listinfo/swift-evolution</a></span><br class=""></div></blockquote></div></div></blockquote></div><br class=""></body></html>