How to Train your Monad
I’ve already shown you how to handle errors and why transforms are great for application architecture. Let’s put all that stuff together to finally achieve our goal: Chaining transforms with error handling.
A short recap of our goal
We wanted to achieve a way of programming that is similar to the unix pipe:
ls | grep .jpg | sort
Methods in this way of programming should have the following characteristics:
- We don’t tell them how to do it, we tell it what we want instead.
- Every command has a well defined interface (
stdin
,stdout
,errout
). - Commands are chainable.
- Error handling is implicit.
Also we defined 4 method signatures for transforms:
// synchronous, non failing
func transform<A,B>(value: A)->B
// async, non failing
func transform<A,B>(value: A, completion: (B->Void))
// synchronous, failable
func transform<A,B>(value: A)->Result<B>
func transform<A,B>(value: A) throws -> B //Swift 2 version
// async, failable
func transform<A,B>(value: A, completion: (Result<B>->Void))
Adding the missing map
We’ve already implemented the synchronous map
function on the Result<T>
.
Let’s add the asynchronous one:
public func map<U>(f:(T, (U->Void))->Void) -> (Result<U>->Void)->Void {
return { g in
switch self {
case let .Success(v): f(v.value){ transformed in
g(.Success(transformed))
}
case let .Error(error): g(.Error(error))
}
}
}
This method takes an asynchronous non failing transform (the 2nd one in our list) and returns a function that can be invoked with a completion block to get the result once it’s completed like in this example:
func toHash(string: String, completion: Int->Void) {
completion(count(string))
}
Result.Success("Hello World").map(toHash)(){ result in
//result is now a .Success of Int with the value 11
}
This might feel a bit clunky at first, but the advantages are pretty obvious as soon as we start to chain synchronous transforms that can fail.
Failable Transforms
Let’s consider applying a failable transform to a Result<T>
:
func toInt(string: String)->Result<Int>{
if let int = string.toInt() {
return .Success(int)
} else {
return .Error(NSError())
}
}
Result.Success("Hello World").map(toInt)(){ result in
// result is now of type Result<Result<Int>>
}
Mapping to a result of a result is actually pretty useless. In the end we’d
prefer to either have success or a failure. Most languages call this feature
flatMap
(because it first maps and then flattens the result), fmap
or bind
.
I’ll go with flatMap
here (there’s something similar on Optional and Array in Swift2).
public func flatMap<U>(f: T -> Result<U>) -> Result<U> {
switch self {
case let .Success(v): return f(v)
case let .Error(error): return .Error(error)
}
}
If the result is a success, the next function is executed, if it’s a failure the error is returned immediateley. The previous example now looks like this:
Result.Success("Hello World").flatMap(toInt)(){ result in
// result is finally of type Result<Int>
}
This looks a lot nicer. There is still one transform missing: The async failable transform:
public func flatMap<U>(f:(T, (Result<U>->Void))->Void) -> (Result<U>->Void)->Void {
return { g in
switch self {
case let .Success(v): f(v, g)
case let .Error(error): g(.Error(error))
}
}
}
This method returns a completion handler that can be used to grab the result once it’s completed:
func toInt(string: String, completion:Result<Int>->Void){
if let int = string.toInt() {
completion(.Success(int))
} else {
completion(.Error(NSError()))
}
}
Result.Success("Hello World").flatMap(toInt)(){ result in
// result is again of type Result<Int>
}
Method Chains
Now we can finally do our unix example:
ls | grep .jpg | sort
let ls = Result.Success(["/home/me.jpg", "home/data.json"])
let grep: String->([String]->Result<[String]>) = { pattern in
return { paths in
return .Success(paths)
}
}
let sort: [String]->Result<[String]> = { values in
return .Success(values.sort())
}
let chain = ls.flatMap(grep(".jpg")).flatMap(sort) //Result<Array>
Let’s revisit our wishlist:
1. We don’t tell them how to do it, we tell it what we want instead.
We’ve written small, composable functions that can be chained together. They’re reusable and easily testable.
2. Every command has a well defined interface
All tansforms take a value, manipulate a copy and either returns a .Success
or
an .Error
.
3. Commands are chainable.
By using map and flatMap we can chain functions and get a result (this is not yet working for async transforms - we’ll do something about that in the next post). By using generics we can be sure that only matching functions can be concatenated (this is actually an advancement over the unix implementation).
4. Error handling is implicit.
If an error happens during a transform, all further transforms are skipped and
an .Error
is returned instead. There is no chance to forget an error handling
branch (and no “I’ll do this later //Fixme:
”).
The monad and you
What we’ve just implemented is also called a monad. A monad is a thing that as a constructor and that defines flatMap. If you want to read more about monads, burritos and boxes take a look at fuckingmonads.com. Also there is a great explanation at Functors, Applicatives, And Monads In Pictures.
A deeper look into space
We’ve seen a great application for synchronous flatMap methods - but handling async
stuff is still missing. In the next chapter we’ll take a look at Signal<T>
and
how to tame callback hell. In the end you will have seen the full implementation
of Interstellar, the reactive
programming framework. We’re almost there!