Disfunctional RSS

Where misquotes, misinformation, and misspellings occur daily.

Archive

Dec
2nd
Sun
permalink

Monads as I understand them

To be honest, it took me a ton of tutorials - a ton - before I could look you in the eye and say I understand what the hell a monad is.

I believe the reason for this is not because they are hard, but because all those tutorials didn’t build upon my basic understanding of functors. Functors are much easier to understand. If you need a refresher read this: http://drboolean.tumblr.com/post/25413244757/functor-i-hardly-knew-her

Or have a play with them yourself: https://github.com/loop-recur/typeclasses

In this post, I hope to take the quickest path to explaining monads without taking all the detours due to their vast usage and abstract nature.

Why we have monads in the first place


What follows is our naive program. In a perfect world we could write this:

//+ getUser :: Number -> User
var getUser = db.find('users');

//+ parseUrl :: String -> Number
var parseUrl = compose(pluck(1), match(/users\/(\d+)/));

//+ urlToUser :: String -> User
var urlToUser = compose(getUser, parseUrl);
Our entry point is urlToUser. It will take a url string to parse and extract the id from then it will give that to getUser to find our user.

I said this representation is naive because there are a few places we can fail. Let’s start with it failing on the parseUrl function. This function may or may not find a match from our match(/users\/(\d+)/) and it will blow up with a TypeError: Cannot read property ‘1’ of null if it can’t!

This will never do. Since every monad is a functor and functors are easier to understand, lets start there. Functors to the rescue!

//+ getUser :: Number -> User
var getUser = db.find('users');

//+ parseUrl :: String -> Maybe(Number)
var parseUrl = compose(fmap(pluck(1)), Maybe, match(/users\/(\d+)/));

//+ urlToUser :: String -> Maybe(User)
var urlToUser = compose(fmap(getUser), parseUrl);
What we had to do is place our match result into a Maybe. This forces the rest of our code to deal with the fact that we may or may not have a match in pluck(1) and getUser.

The real thing to think about here is what we’ve done typewise. In urlToUser, we’ve composed parseUrl :: String -> Maybe(Number) with getUser :: Number -> User. Those types shouldn’t line up since getUser takes a Number not a Maybe(Number). Through the magic of fmap which opens up our Maybe and runs the function on it’s innards, we pass our Number to getUser instead of the Maybe(Number) and all is well.

You could say we composed these types:

a -> F(b) (parseUrl)

b -> c (getUser)

And ended up with:

a -> F(c) (urlToUser)

Where F is the functor (in this case Maybe). K, great. We understand functors and all that, but what about monads?!

Getting more robust


Well, we caught the first failure case, but there’s another one. What if getUser doesn’t actually find a user?! Total code anarchy. Let’s catch this case now*:

//+ getUser :: Number -> Maybe(User)
var getUser = compose(Maybe, db.find('users'));

//+ parseUrl :: String -> Maybe(Number)
var parseUrl = compose(fmap(pluck(1)), Maybe, match(/users\/(\d+)/));

//+ urlToUser :: String -> Maybe(Maybe(User))
var urlToUser = compose(fmap(getUser), parseUrl);
Darn it. We wanted urlToUser to return a Maybe(User), but we returned a Maybe(Maybe(User)). Since fmap re-wraps up the type for us and getUser returns a Maybe too, we get a nested Maybe that we need to flatten. This is precisely the time to use a monad. Luckily, Maybe is a monad in addition to being a functor.

There is this great function called mjoin that we need to use. It’s specific to each monad instance. For Maybe it works like this:

mjoin(Maybe(Maybe("hello monad"))) // Maybe("hello monad")

mjoin(Maybe(Maybe(null))) // Maybe(null)
	

//+ urlToUser :: String -> Maybe(User)
var urlToUser = compose(mjoin, fmap(getUser), parseUrl);
Boom! We just flattened the instance and we’re good to go. Congrats, you just used the Maybe monad! It was really that easy.

So back to types. We needed to compose parseUrl :: String -> Maybe(Number) with getUser :: Number -> Maybe(User). This is exactly the pattern we want to recognize as monadic composition or in other words, composing two functions that take normal values, but return monadic values. Here’s the abstract pattern:

a -> M(b) (parseUrl)

b -> M(c) (getUser)

And ended up with:

a -> M(c) (urlToUser)

We used M for monad in the types above instead of F for functor since we’ll need the ability to call mjoin on it afterwards to flatten it up.

So monads are functors that flatten?


Well, yes…but it goes further than that. Just stay with me here as we go through the final trick.

If we wanted to compose lots of monadic functions it would get real awkward real fast since mjoin only flattens our values once and we’d see a lot of compose(mjoin, fmap) everywhere.

The real power of monads comes from our mbind function. This function is defined as just the combination of fmap and mjoin as you would expect, but with a few quirks:

//+ mbind :: M(a) -> (a -> M(b)) -> M(b)
var mbind = function(mv, f) {
  return compose(mjoin, fmap(f))(mv);
}
The first thing to notice is we’ve flipped the arguments to around (value, then function). This is because it’s setup for chaining/nesting. Check it out:

//+ addMaybes :: Maybe(Number) -> Maybe(Number) -> Maybe(Number)
var addMaybes = function(ma, mb) {
  return mbind(ma, function(a) {
    return mbind(mb, function(b) {
      return Maybe(a + b);
    });
  });
}

addMaybes(Maybe(3), Maybe(4)) // Maybe(7)
addMaybes(Maybe(null), Maybe(4)) // Maybe(null)
This is just like our node.js “pyramid of doom” nested callback style. The reason that’s so powerful is that:

1. We can depend on the values from the previous expressions

We get access to a and b to use together. We can keep mbind-ing deeper and deeper and pretend as though all the values have succeeded and are there.

2. We can prevent the nested functions from running all together

If anything isn’t there, we’ll never reach the following function since Maybe won’t even run it. And since the rest of the functions are nested the whole process gets shut down right then and there.

Both of these features are unique to monads and are what make a them more powerful than functors/applicatives. So use that when you’re making a decision on whether or not you need to use a monad or not. Usually it’s better to not use them if you can get away with something simpler.

Lastly, …for now


There’s a whole lot of monad stuff to learn. I could talk about pointed functors and keeping it type generic by using mresult, I could talk about mcompose or ap or the monoid instance MonadPlus, but I wanted to point out one last little feature for now. liftM

liftM keeps us point free and simple. It works just like fmap, but for monads:

//+ addMaybes :: M(Number) -> M(Number) -> M(Number)
var addMaybes = liftM("+");

liftM(function(x){ return x + 1; }, Maybe(4)) // Maybe(5)

liftM(function(x, y){ return x + y; }, Maybe(4), Maybe(5)) // Maybe(9)

liftM(function(x, y){ return x + y; }, Maybe(4), Maybe(null)) // Maybe(null)
When liftM is given multiple arguments, it nests each value as if we were writing our mbind manually.

So there you have it!

*Something to mention is that db.find should probably return a Maybe(a) by design. This would force us to deal with the nulls and make our program more robust from the start. If you contrast that to just returning null like we did in the previous example, you can see that these types really do help you write a more correct program.
  1. liamgoodacre reblogged this from drboolean
blog comments powered by Disqus