Either is Both
July 26, 2018
We started our exploration of Optionals with a function that accepted an Int. If the Int was even, the function returned it. If it wasn't even the function threw an error.
extension String: Error{}
func echoEvenOrThrow(_ input: Int) throws -> Int {
guard input.isEven else { throw "\(input) is odd"}
return input
}
The good news was that we got great information if the the function failed.
try echoEvenOrThrow(3)
results in
Playground execution terminated: An error was thrown and was not caught:
"3 is odd"
Unfortunately, any function that calls echoEvenOrThrow has to catch and handle or rethrow the error.
We found that Optionals were a great solution. We just return nil if there's an error.
func echoEvenOrNil(_ input: Int) -> Int? {
guard input.isEven else {return nil}
return Optional(input)
}
We can now use Optional's map() function to chain functions like this with functions that have no idea about Optionals at all. For example, here's how we connect to halve().
echoEvenOrNil(3).map(halve)
This works great in our workflow but we've lost any information on what went wrong.
In this post we'll implement a Result type that combines the information on what the error was with the ability to use map() to pipe a Result instance into a function that knows nothing about Results.
Today's code comes from this sample code and here's the Playground it comes from.
A Result is an enum with two cases: success and failure (of error).
extension String: Error{}
enum Result<Value> {
case failure(Error)
case success(Value)
}
You can see that this looks a lot like our enumeration implementation of Optional but the failure case contains information on what the error is.
Let's write a version of our echo function that returns a Result.
func echoEvenOrFail(_ input: Int) -> Result<Int> {
guard input.isEven else { return .failure("\(input) is odd")}
return .success(input)
}
Test it out with an even and an odd.
echoEvenOrFail(2)
echoEvenOrFail(3)
The results are success(2) and failure("3 is odd").
As before, we'd like to send the results into the halve() function. halve has no idea about Result.
func halve(_ input: Int) -> Int {
return input/2
}
Take some inspiration from our map() function from Opt. The success case for Result looks the same as the some case for Opt. The difference is in the failure case. As with the none case, a failure results in a failure. The additional piece is that we have to pass the error on.
extension Result {
func map<Output>(_ f: @escaping (Value) -> Output)
-> Result<Output> {
switch self {
case .failure(let errorMessage):
return .failure(errorMessage)
case .success(let value):
return .success(f(value))
}
}
}
Look how easy it is to use our map() and still see the error.
echoEvenOrFail(2).map(halve)
echoEvenOrFail(3).map(halve)
The results are success(1) and failure("3 is odd").
Wait - there's more fun to come.
Let's re-introduce the numberDictionary and adapt the function valueInNumberDictionary to return a Result<String>.
let numberDictionary = [1: "one", 2: "two", 3: "three"]
func valueInNumberDictionary(for key: Int) -> Result<String> {
guard numberDictionary.keys.contains(key) else
{return .failure("Dictionary has no key: \(key)")}
return .success(numberDictionary[key]!)
}
Looking at the signatures of the echoEvenOrFail() and valueInNumberDictionary() functions we see that we need a flatMap(). Again we can adapt the one we wrote for Opt.
extension Result {
func flatMap<Output>(_ f: @escaping (Value)
-> Result<Output> )
-> Result<Output> {
switch self {
case .failure(let errorMessage):
return .failure(errorMessage)
case .success(let value):
return f(value)
}
}
}
Let's take it for a ride.
echoEvenOrFail(2).flatMap(valueInNumberDictionary)
echoEvenOrFail(3).flatMap(valueInNumberDictionary)
echoEvenOrFail(4).flatMap(valueInNumberDictionary)
The results are respectively, success("two"), failure("3 is odd"), and failure("Dictionary has no key: 4").
At some point we'll return to this topic and look at Monoids and Applicatives so we can actually combine errors. But that's it for our quick series on errors, optionals, and result types.