> On Feb 23, 2017, at 12:24 PM, Karl Wagner <[email protected]> wrote: > > 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). > > - Karl > >> On 23 Feb 2017, at 19:09, Matthew Johnson via swift-evolution >> <[email protected] <mailto:[email protected]>> 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 >>> <[email protected] <mailto:[email protected]>> 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 >>> [email protected] <mailto:[email protected]> >>> https://lists.swift.org/mailman/listinfo/swift-evolution >> >> _______________________________________________ >> swift-evolution mailing list >> [email protected] <mailto:[email protected]> >> https://lists.swift.org/mailman/listinfo/swift-evolution >
_______________________________________________ swift-evolution mailing list [email protected] https://lists.swift.org/mailman/listinfo/swift-evolution
