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
_______________________________________________ swift-evolution mailing list swift-evolution@swift.org https://lists.swift.org/mailman/listinfo/swift-evolution