[swift-users] Rethrows issue

Howard Lovatt howard.lovatt at gmail.com
Mon Jan 1 22:54:27 CST 2018


I don’t get why that type checks:

  1. rescue always throws inside sync, therefore
  2. _syncHelper inside sync always throws, therefore
  3. sync always throws, therefore
  4. Shouldn’t sync be declared throws not rethrows?

-- Howard. 

On 1 Jan 2018, at 1:10 pm, Charles Srstka via swift-users <swift-users at swift.org> wrote:

>> On Dec 31, 2017, at 1:35 AM, Nevin Brackett-Rozinsky via swift-users <swift-users at swift.org> wrote:
>> 
>> So, I know this doesn’t actually address your issue, but I think it is important to clarify that “rethrows” does not guarantee anything about the *type* of error thrown by a function. What “rethrows” implies is that the function *will not throw* unless at least one of its arguments throws.
>> 
>> In particular, if the argument throws, the outer function can throw *any error or none at all*. For example,
>> 
>> enum CatError: Error { case hairball }
>> enum DogError: Error { case chasedSkunk }
>> 
>> func foo(_ f: () throws -> Void) rethrows -> Void {
>>     do    { try f() }
>>     catch { throw CatError.hairball }
>> }
>> 
>> do {
>>     try foo{ throw DogError.chasedSkunk }
>> } catch {
>>     print(error)    // hairball
>> }
>> 
>> Inside foo’s catch block, it is legal to throw any error, or not throw an error at all. But *outside* that catch block foo cannot throw, which is causing you consternation.
>> 
>> • • •
>> 
>> I don’t have a good solution for you, but in attempting to find one I *did* uncover something which compiles that probably shouldn’t. It seems that a “rethrows” function is currently allowed to throw if a *local* function throws:
>> 
>> func rethrowing(_ f: () throws -> Void) rethrows -> Void {
>>     func localThrowing() throws -> Void { throw CatError.hairball }
>>     return try localThrowing()
>> }
>> 
>> do {
>>     try rethrowing{ throw DogError.chasedSkunk }
>> } catch {
>>     print(error)    // hairball
>> }
>> 
>> I wouldn’t count on this functionality as it is most likely a bug. Indeed, if we pass in a non-throwing argument then we get a runtime error:
>> 
>> rethrowing{ return }  // EXC_BAD_ACCESS (code=1, address=0x0)
>> 
>> Although, if we change “localThrowing” to use do/catch on a call to “f” and throw only in the catch block, or even use your “var caught: Error?” trick, then it appears to work as intended with no problems at runtime.
>> 
>> • • •
>> 
>> In the unlikely scenario that the above local-function behavior is valid and intended, the following function will, technically speaking, let you work around the issue you’re having:
>> 
>> func withPredicateErrors <Element, Return>
>>     (_ predicate: (Element) throws -> Bool,
>>      do body: @escaping ((Element) -> Bool) -> Return
>>     ) rethrows -> Return
>> {
>>     func bodyWrapper(_ f: (Element) throws -> Bool) throws -> Return {
>>         var caught: Error?
>>         let value = body{ elem in
>>             do {
>>                 return try f(elem)
>>             } catch {
>>                 caught = error
>>                 return true
>>             }
>>         }
>>         if let caught = caught { throw caught }
>>         return value
>>     }
>> 
>>     return try bodyWrapper(predicate)
>> }
>> 
>> It is not pretty, and it probably relies on a compiler bug, but at the present time, against all odds, it look like this operates as you intend.
>> 
>> Nevin
> 
> That’s not the only exploit of that sort to exist in the frameworks, actually—and if you look through the source code to the standard library, you’ll see that the Swift standard library actually uses this kind of thing from time to time. For example, take DispatchQueue.sync(), which is declared as ‘rethrows’ despite wrapping a C function that Swift can’t reason anything about. I’d always wondered how that worked, so at some point I looked it up. Here’s how it's implemented:
> 
> The public function we’re all used to, sync():
> 
> public func sync<T>(execute work: () throws -> T) rethrows -> T {
>     return try self._syncHelper(fn: sync, execute: work, rescue: { throw $0 })
> }
> 
> This basically passes everything on to a private method, _syncHelper, which looks like this:
> 
> private func _syncHelper<T>(
>              fn: (() -> Void) -> Void,
>              execute work: () throws -> T,
>              rescue: ((Error) throws -> (T))) rethrows -> T
> {
>     var result: T?
>     var error: Error?
>     withoutActuallyEscaping(work) { _work in
>         fn {
>             do {
>                 result = try _work()
>             } catch let e {
>                 error = e
>             }
>         }
>     }
>     if let e = error {
>         return try rescue(e)
>     } else {
>         return result!
>     }
> }
> 
> So basically, since every error thrown inside _syncHelper() is rethrown from one of the closures passed into it, that satisfies ‘rethrows’, and it also satisfies ‘rethrows’ for sync(), since anything thrown by it is thrown by another method that’s also declared “rethrows”. Sneaky.
> 
> Anyway, if you really need to do something like this, I’d recommend doing it the way the Swift standard library does, because:
> 
> 1. They can’t break that in the compiler without also breaking their own code, and:
> 
> 2. If they *do* break it because they’ve introduced a new, proper way to do this using some kind of actuallyRethrows() function or something, you’ll want to switch to that anyway.
> 
> Charles
> 
> _______________________________________________
> swift-users mailing list
> swift-users at swift.org
> https://lists.swift.org/mailman/listinfo/swift-users
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-users/attachments/20180101/603d2e73/attachment.html>


More information about the swift-users mailing list