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.

2 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.

1 Like

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.

2 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?

Terms of Service

Privacy Policy

Cookie Policy