I've been playing with typed throws recently for a little library I've been experimenting on and have been butting against the current limitations of both typed throws and rethrows
.
Looking at the discussions for the typed throws feature I'm having thoughts on what direction we could move rethrows
towards that wouldn't paint us into a corner while making itself far more useful than it currently is. Let's run through them.
Current rethrows
As currently implemented, rethrows
for the following function
func rethrowing<A, B>(a: () throws(A) -> Void, b: () throws(B) -> Void) rethrows
Means that if A != Never || B != Never
the function throws(any Error)
, else it throws(Never)
Even if there is a single type parameter, the function will still throws(any Error)
, at least on the currently released compiler.
So there's two parts to what rethrows
does:
- Determine whether a generic function
throws
- What type of error it throws.
We're not going to discuss #2 and in fact I believe we should leave rethrows
out of that. So all further discussion here omits what is being thrown.
Step 1: rethrows
as a marker of when a function throws
Consider the following:
func doesTwoThings<A, B>(a: () throws(A) -> Void, b: () throws(B) -> Void) rethrows {
do {
try a()
} catch {
print("a threw an error. Whatevs")
}
try b()
}
This function will insist on declaring itself as throwing even if b
does not throw
The obvious solution would be to be able to specify what types cause rethrows
to kick in, as follows.
func doesTwoThings<A, B>(a: () throws(A) -> Void, b: () throws(B) -> Void) rethrows(B) {
do {
try a()
} catch {
print("a threw an error. Whatevs")
}
try b()
}
Step 2: rethrows
+ typed throws
So far so good. Let's take it further
struct DoesTheThing<Failure> {
func doTheThing() throws(Failure) { ... }
func mayThrow<A>(a: () throws(A) -> Void) throws(Failure) {
do {
try a()
} catch {
print("a threw an error. Whatevs")
}
try doTheThing()
}
So far so good. But it does have some unfortunate limitations since it forces us to throws(Failure)
so
struct Wrapped<W: Error>: Error {
let wrapped: W
}
struct DoesTheThing<Failure> {
func doTheThing() throws(Failure) { ... }
func mayThrow<A>(a: () throws(A) -> Void) throws(Failure) {
do {
try a()
} catch {
print("a threw an error. Whatevs")
}
do {
try doTheThing()
} catch {
throw Wrapped(wrapped: error)
}
}
Uh, compiler justifiably complains that we're trying to throw something other than Failure
.
But if we declare mayThrow
as throws(Wrapped<Failure>)
then the compiler will thing it has to throw even when Failure == Never
Vanilla rethrows
also won't do as the parameter has nothign to do with whether the function should throw or not.
Here we would want something like the following:
struct DoesTheThing<Failure> {
func doTheThing() throws(Failure) { ... }
func mayThrow<A>(a: () throws(A) -> Void) rethrows(Failure) throws(Wrapped<Failure>) {
do {
try a()
} catch {
print("a threw an error. Whatevs")
}
do {
try doTheThing()
} catch {
throw Wrapped(wrapped: error)
}
}
Assuming of course that we want mayThrow
to have a typed throw
. If rethrows
doesn't match then the compiler can ignore the type of throws
, and if we're going to throws(any Error)
we can omit it as well.
Step 3: Basic multiple types
If there's more than one type involved in determining whether a function should throw or not, the basic idea would be to just add a list of types and if any of them is not Never
then the function may throw
.
struct DoesTheThing<Failure> {
func mayThrow<A, B>(a: () throws(A) -> Void, b: () throws(B) -> Void) rethrows(Failure, B) {
// We're going to skip the implementation now…
}
}
This matches the current behavior for rethrows
with more than one block parameter that may throw.
Step 4: Type Algebra
Let's not go there yet.
Enough Thoughts for Today
There's a few questions we'd want to answer before trying for a proper pitch, let alone an implementation:
- Would this disrupt existing code? Doesn't look like it. Non-typed
rethrows
can maintain its current behavior. - Do all these rules make sense together? Please find any holes in my logic, I'm not the best person to see them if they're there.
- Does this look like something that can be banged into the compiler without too much (comparatively speaking) effort? I'm far from the most qualified person to answer this one.
- Does this approach paint us into a corner against future developments? We weren't doing much useful with
rethrows
especially now that typed throws exist. Separating typedrethrows
from typedthrows
also lets us be more incremental.