Failure is an Option(al)
July 20, 2018
Suppose we have a simple function that either succeeds or it doesn't.
In this post we look at why optionals and result types might be nicer for functional style programming than errors. There's nothing deep or magical about this post and it's not the way I teach Optionals - but this is the beginning of a second sequence of articles about functional programming. In this sequence I'm thinking about composing behavior from a series of functions.
Today's code comes from this sample code and here's the Playground it comes from.
Here's some pseudo-code for a simple function that returns the inputted Int if it's even and fails if it's odd.
func echoEvensOnly(_ input: Int) -> Int {
if input is even {return input}
else {something has gone wrong}
}
Depending on what language we come from, our first instinct might be to throw an error or exception.
This is probably not a good instinct but let's follow it for a moment.
At the top of the code you'll note I've added a couple of items for convenience. There's an isEven computed property on Int that returns true if the int is even and false otherwise. I've also declared that String conforms to Error to make it easy for us to create errors from strings.
extension Int {
var isEven: Bool {
return self % 2 == 0 ? true : false
}
}
extension String: Error{}
So here's a version of our echo function that returns an even input and throws an error if we submit an odd input.
func echoEvenOrThrow(_ input: Int) throws -> Int {
guard input.isEven else { throw "\(input) is odd"}
return input
}
Pass an even number to echoEvenOrThrow.
echoEvenOrThrow(2)
You'll see 2 on the right of the playground as your feedback.
Pass an odd number to echoEvenOrThrow.
echoEvenOrThrow(3)
You'll see the error in the console:
Playground execution terminated: An error was thrown and was not caught:
"3 is odd"
Any code that calls echoEvenOrThrow() should do so using try.
do {
try echoEvenOrThrow(2)
} catch {
print("error:", error)
}
This results in 2, while this
do {
try echoEvenOrThrow(3)
} catch {
print("error:", error)
}
results in "error: 3 is odd\n".
In any case, it is difficult to fit echoEvenOrThrow() into a nice chain of function calls because it either returns an Int or it throws and error.
We could, of course, use try? which wraps the call to echoEvenOrThrow() in an Optional.
try? echoEvenOrThrow(2)
try? echoEvenOrThrow(3)
Next to the first line you'll see 2 and next to the second you'll see nil. I've said before that I find the 2 misleading as actually it is .some(2). In other words, the result of try? echoEvenOrThrow() is an Optional<Int>.
The nice thing about try? is it converts a function that returns a value or throws and error into one that always returns a value of a given type. The penalty we pay is that even when it is successful the value that is returned is wrapped inside an Optional.
Let's replace echoEvenOrThrow() with echoEvenOrNil().
func echoEvenOrNil(_ input: Int) -> Int? {
guard input.isEven else {return nil}
return input
}
Call this and pass in 2 and the playground echos 2. Pass in 3 and the playground echos nil. Again, this reminds us that the 2 is really a wrapped value.
echoEvenOrNil(2)
echoEvenOrNil(3)
Hmmm. Look again at the highlighted line below.
func echoEvenOrNil(_ input: Int) -> Int? {
guard input.isEven else {return nil}
return input
}
We seem to be returning input which is an Int and not an Int?.
Swift sees that our function signature requires that we return an Int? and quietly does the wrapping for us.
If you'd like, we can explicitly wrap the value like this:
func echoEvenOrNil(_ input: Int) -> Int? {
guard input.isEven else {return nil}
return .some(input)
}
or like this:
func echoEvenOrNil(_ input: Int) -> Int? {
guard input.isEven else {return nil}
return Optional(input)
}
We'll stick to the first version for now.
func echoEvenOrNil(_ input: Int) -> Int? {
guard input.isEven else {return nil}
return input
}
Other languages call the Optional type the Maybe type. We'll look into it a bit more in the next article in this series to see how we might chain functions together that emit optionals.
Other languages often also have an Either type to represent two very different results from performing an operation. Some call this (or specialize it to) a Result type. The Either type has a left and right where often the left side represents the failing state and the right side represents the successful state.
Swift doesn't have a built in Result type though many people have asked for one. You'll usually see it implemented in Swift one of two ways. The one we'll look at here uses Swift's built in Error protocol.
enum Result<Value> {
case failure(Error)
case success(Value)
}
Create a version of the echo function that returns a Result.
func echoEvenResults(_ input: Int) -> Result<Int> {
guard input.isEven else {return Result.failure("\(input) is odd")}
return Result.success(input)
}
This will compose nicely as a method because it always returns something of type Result<Int>. Use it with 2 and 3 as before.
echoEvenResults(2)
echoEvenResults(3)
For 2 we see the result success(2) and for 3 we see failure("3 is odd").
In the next installment we'll take a closer look at Optional and Result and how we work with functions that return them.