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