Pitch: `rethrows(unchecked)`

rethrows lets you specify that a function can only throw if one of the functions passed to it as a parameter can throw. It enforces this by only allowing try to be applied to calls to those functions or rethrows functions which are being passed those functions, and only allowing throws inside a catch block.

However, this enforcement can sometimes get in the way. For example, this function only throws if the function it is passed throws, but the compiler cannot statically prove this to itself:

function someOperation<R>(do body: () -> throws R) -> rethrows R {
  var result: R?
  var error: Error?
  // __someOperation is a version of the operation which doesn't support 
  // returning a value or throwing. This happens a lot when you're 
  // wrapping Objective-C.
  __someOperation {
    do { result = try body() }
    catch let e { error = e }
  }
  if let error = error { throw error }
  return result!
}

It is possible to work around this by exploiting certain bugs in the rethrows checking—the Dispatch overlay does this to add error handling to DispatchQueue.sync(execute:)—but this is not ideal for obvious reasons.

I propose that we add a rethrows(unchecked) variant of the keyword which says that the function promises it will only throw if one of the throws functions passed to it actually throw, but disables the compile-time checking of the function body. This would allow us to cleanly write wrappers like this one.

An alternative might be to build an equivalent to withoutActuallyEscaping(_:do:), but I can't come up with a good name for it (withoutActuallyThrowingUnlessRethrowing(from:do:)?), and I think I had trouble prototyping such a thing when I tried a couple of weeks ago.

I can't work on this immediately, but I wanted to see if people were interested in this and what our preferred design would be if we had it.

3 Likes

I think this is the more "Swifty" way of expressing the idea. I'd suggest something like withActuallyOnlyRethrowing(from:do:). But yes, seems like a reasonable facility to have for interop purposes.

2 Likes

The functionality sounds great to me. I found myself wanting this when we originally added rethrows to the standard library. I agree with @xwu that a withoutActually kind of escape hatch block makes more sense than a declaration-level decoration.

3 Likes

Is it possible to verify this condition dynamically, so that debug builds could still check that it holds? The reason we didn't put "unsafe" in the name of "withoutActuallyEscaping" was because we know how to write that check, even if we haven't implemented it yet.

2 Likes

Good question! Seems like it should be. When we invoke the passed-in function argument, we could save the error result in a variable that we assert is non-nil on any paths out of the outer function that result in an error.

Ah, but it's not "does not throw". It's "does not throw if the arguments don't throw". Do we record that information in the closure info?

EDIT: Whoops, I think I misunderstood the thing that would get checked. We'd wrap the callbacks rather than the rethrowing function?

I'm curious about solutions to another rethrows problem: building higher-order functions/methods that can accept re-throwing functions.

Here's an example that doesn't work:

extension Optional {
  public func apply<A>(_ f: ((Wrapped) -> A)?) -> A? {
    return f.flatMap(self.map)
  }
}

The compiler error:

Call can throw, but it is not marked with 'try' and the error is not handled

It's understandable, but unfortunate. We're passing non-throwing f along and even though nothing throwing is in scope, Swift can't figure it out.

How about a more complicated example? Here's a zip that flattens certain pyramids of death:

func zip<A, B, R>(
  _ f: @escaping ((A) throws -> R) throws -> R,
  _ g: @escaping ((B) throws -> R) throws -> R,
  with: (A, B) -> R
  ) throws -> R {

  return try f { a in
    try g { b in
      with(a, b)
    }
  }
}

We have to promote rethrows to throws for another compiler error:

Only function declarations may be marked 'rethrows'; did you mean 'throws'?

An example (toy) use, which unfortunately requires do/try if we can't address throws/rethrows:

func someCFunction(_ x: UnsafeRawBufferPointer, _ y: UnsafeRawBufferPointer) -> Int {
  return 1
}

let xs = [1, 2]
let ys = [3, 4]

try zip(xs.withUnsafeBytes, ys.withUnsafeBytes, with: someCFunction)

It'd be nice to have an escape hatch for library authors to compose rethrows more generally.

4 Likes

So, did anything come of this?

For what it's worth, I'm running into the higher-order function problem, implementing type erasure. Reduced example:

protocol P {
  associatedtype A
  func invoke(_ body: () throws->()) rethrows
}

/// Type-eraser with nonthrowing closure.
struct AnyP0 {
  struct A {}
  let value: Any
  let invoker: (Self, _ body: () throws->()) -> Void

  init<T: P>(_ x: T) {
    value = x
    invoker = { self_, body in try! (self_.value as! T).invoke(body) }
  }
}

extension AnyP0: P {
  /// - WARNING: This signature lies and claims it supports throwing functions,
  ///   but it does not; if `body` throws, this method will trap!
  func invoke(_ body: () throws->()) rethrows {
    invoker(self, body)
  }
}

/// Type-eraser with throwing closure.
struct AnyP1 {
  struct A {}
  let value: Any
  let invoker: (Self, _ body: () throws->()) throws -> Void

  init<T: P>(_ x: T) {
    value = x
    invoker = { self_, body in try (self_.value as! T).invoke(body) }
  }
}

extension AnyP1: P {
  /// - WARNING: This signature lies; all errors thrown by body will be
  ///   swallowed.  
  func invoke(_ body: () throws->()) rethrows {
    do {
      try invoker(self, body)
    }
    catch let e {
      // throw e <-- error: a function declared 'rethrows' may only throw if its parameter does
      _ = e // only choice is to swallow the error.
    }
  }
}

In my case, it might be possible to solve the problem with rethrows as a first-class type annotation, but I don't think all cases will work that way, so there should be an escape clause in the library to deal with the places where the knowledge is really only available to the programmer.

1 Like