> 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

Reply via email to