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

Reply via email to