Last time we talked about how a function that can throw errors is a different type in Swift than a function that cannot throw errors. And then I briefly mentioned this other thing, “rethrows.” Let’s talk about that, and along the way explore closure types a little more and their weird and woolly ways.
Like last time, we start with
mymap so there’s no confusion with the
1 2 3 4 5 6 7 8 9
So that’s the simple
map. As we discussed previously, we can’t pass a throwing
closure to it because it would be the wrong type. Let’s rewrite
mymap so it
1 2 3 4 5 6 7 8 9
transforms can throw, and so it needs
try when we call it. And since we
don’t handle the error ourselves, the whole method has to be marked
Let’s create a couple of functions to check this out:
1 2 3 4 5 6 7 8
The first function,
double, always succeeds. The second function,
reciprocal, may throw.
1 2 3
And if we pass them to the other methods?
1 2 3
So we can pass a non-throwing closure to the throwing
map, but not vice versa.
Why? Let’s take a step back and talk about subtypes.
A good way to think about types is as a set of promises. In the OOP world, we create types like this:
1 2 3 4 5 6 7
Every Animal promises it can eat. Every Cat promises it can purr. Since a Cat is an Animal, it also promises it can eat. But not every Animal promises to purr (other Animals may be able to purr, it’s just not promised). You’re used to calling Cat a subclass of Animal, and that’s true. But it’s more generally a subtype. This idea isn’t restricted to classes. After all, the same thing is true of protocols:
1 2 3 4 5 6 7
No classes required. The important thing about the type/subtype relationship is that a subtype can only add promises. It can never remove promises. Understanding what promises are being made is very important to understanding your types.
NSArray doesn’t promise to be immutable. That may surprise you, but you know
it’s true because you copy them when they’re passed as parameters. If
promised to be immutable (like
NSDate does), you’d never do that. If
promised to be immutable, then
NSMutableArray couldn’t be its subclass,
because it breaks that promise.
NSArray only promises to be readable. That’s a completely different thing.
NSMutableArray also promises to be readable. It keeps the promise
NSMutableArray also promises to be writable, and any subclass of
NSMutableArray would also have to keep that promise.
A subtype can only add promises. It can never remove them.
So, what promises does
(T) throws -> U make? It promises to accept a
it promises that it will either return a
U or it will throw an error.
What promises does
(T) -> U make? It promises to accept a
it promises that it will return a
How are these types related? Which one makes the stronger promise? A good way to figure this out is to think through some cases.
- Function returns
U. Keeps both promises.
- Function throws an error. Keeps one promise, breaks the other.
The stronger promise is the one that we broke. It’s the non-throwing function that added a new, stricter promise. “I will do X or Y, and furthermore I will only do X.” Doing X keeps that promise. Doing Y breaks it.
So that tells us that a non-throwing closure can be used anywhere a throwing closure is requested, just like a Cat can be used anywhere an Animal is requested. No conversions necessary. It’s just types.
So great, we have
mymapThrows, and it takes either kind, so we’re done, right?
Well, we could be, but it’d be really annoying. Consider if
map were marked
throws. That would mean that every
map would have to include a
somewhere you’d have to catch the error.
1 2 3 4 5 6 7
There are two ways out of this annoyance. The obvious way is overloading. We can just have two methods with the same name but different types:
Since overloading picks the most specific subtype available, this works fine for
the caller. But it’s a serious pain for the dev who has to write
the obvious annoyance of needing two methods to do the job of one, but it gets
worse if you try to share code between the implementations. You’d think you
could just call the throwing version from the non-throwing version like:
1 2 3
But that runs afoul of
@noescape, which doesn’t allow the conversion. And even
if that worked (might be a Swift bug), having to use
try! all over the place
is crazy, on top of the madness of having two (or three) methods for everything.
My overload implementation looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
If Swift had shipped this way, I suspect the stdlib folks would be having words with the compiler folks by now. “Please come over to my desk. I’d like to introduce you to another kind of throws.”
Luckily, Swift is much smarter than that. It’s nice that you can overload based
on throwing, but in many cases we have a better tool. We can mark the method
rethrows rather than
func map<T>(@noescape transform: (Generator.Element) throws -> T) rethrows -> [T]
So what promise does
rethrows make? It promises that the only way it will
throw is if a closure it is passed throws. So if it’s passed a closure that
can’t throw, the compiler knows that the function can’t throw either.
(Why isn’t stdlib’s
rethrows today? Because it’s beta 1, and the
Swift team hasn’t updated all of stdlib yet. They’ve indicated that a lot of
stdlib will be fixed in future betas. Have patience.)
It’s natural to think of
rethrows as a subtype of
throws, and non-throwing
closures as a subtype of
rethrows, but that doesn’t quite seem to be true.
Swift doesn’t treat
rethrows as a full type. For example, you can’t write
overloads with both
rethrows, and closures can’t include
rethrows in their type. Instead,
rethrows acts more like a function
@noreturn). It just modifies the rules around what contexts
can call the function. The real types are throwing and non-throwing, and
“rethrowing” can just morph between the two based on context.
A function that accepts a closure has three throwing options:
It can throw. That means that the function may throw errors whether or not the closure throws errors.
It can rethrow, like
map. This means that the function cannot create any errors of its own, but may propagate errors from the closure it was passed.
It can not throw. That means that it either handles the errors thrown by the closure, or it does not evaluate the closure. For example, a setter on a closure property doesn’t throw just because the closure might throw. It just sets the property and returns.
Which one you use is completely dependent on your situation. There’s no “best”
answer, though you should generally choose the most restrictive one you can. You
shouldn’t just make all your functions
throws for the same reasons you
shouldn’t make all your variables
Any. It’s all about choosing the right type.
So when you use the new Swift error handling system, don’t think “exceptions.” Think types. Your function returns “either X or an error.” And sometimes, you can promise it’ll only return X.
Throw in peace.