A Fistful of Monads
When we first talked about functors, we saw that they were a useful concept for values that can be mapped over. Then, we took that concept one step further by introducing applicative functors, which allow us to view values of certain data types as values with contexts and use normal functions on those values while preserving the meaning of those contexts.
In this chapter, we'll learn about monads, which are just beefed up applicative functors, much like applicative functors are only beefed up functors.
When we started off with functors, we saw that it's possible to map functions over various data types. We saw that for this purpose, the Functor type class was introduced and it had us asking the question: when we have a function of type a -> b and some data type f a , how do we map that function over the data type to end up with f b ? We saw how to map something over a Maybe a , a list [a] , an IO a etc. We even saw how to map a function a -> b over other functions of type r -> a to getfunctions of type r -> b . To answer this question of how to map a function over some data type, all we had to do was look at the typeof fmap :
- fmap :: ( Functor f) => (a -> b) -> f a -> f b
fmap :: (Functor f) => (a -> b) -> f a -> f b
And then make it work for our data type by writing the appropriate Functor instance.
Then we saw a possible improvement of functors and said, hey, what if that function a -> b is already wrapped inside a functor value? Like, what if we have Just (*3) , how do we apply that to Just 5 ? What if we don't want to apply it to Just 5 but to a Nothing instead? Or if we have [(*2),(+4)] ,how would we apply that to [1,2,3] ? How would that work even? For this, the Applicative type class was introduced, in which we wanted the answer to the following type:
- (<*>) :: ( Applicative f) => f (a -> b) -> f a -> f b
(<*>) :: (Applicative f) => f (a -> b) -> f a -> f b
We also saw that we can take a normal value and wrap it inside a data type. For instance, we can take a and wrap it so that it becomes a Just 1 . Or we can make it into a [1] . Or an I/O action that does nothing and just yields. The function that does this is called pure .
Like we said, an applicative value can be seen as a value with an added context. A fancy value, to put it in technical terms. For instance, the character 'a' is just a normal character, whereas Just 'a' has some added context. Instead of a Char , we have a Maybe Char ,which tells us that its value might be a character, but it could also be an absence of a character.
It was neat to see how the Applicative type class allowed us to use normal functions on these values with context and how that context was preserved. Observe:
- ghci> (*) <$> Just <*> Just
- Just
- ghci> (++) <$> Just "klingon" <*> Nothing
- Nothing
- ghci> (-) <$> [ , ] <*> [ , , ]
- [ , , , , , ]
ghci> (*) <$> Just 2 <*> Just 8Just 16ghci> (++) <$> Just "klingon" <*> NothingNothingghci> (-) <$> [3,4] <*> [1,2,3][2,1,0,3,2,1]
Ah, cool, so now that we treat them as applicative values, Maybe a values represent computations that might have failed, [a] values represent computations that have several results (non-deterministic computations), IO a values represent values that have side-effects, etc.
Monads are a natural extension of applicative functors and with them we're concerned with this: if you have a value with a context, m a , how do you apply to it a function that takes a normal a and returns a value with a context? That is, how do you apply a function of type a -> m b to a value of type m a ? So essentially, we will want this function:
- (>>=) :: ( Monad m) => m a -> (a -> m b) -> m b
(>>=) :: (Monad m) => m a -> (a -> m b) -> m b
If we have a fancy value and a function that takes a normal value but returns a fancy value, how do we feed that fancy value into the function? This is the main question that we will concern ourselves when dealing with monads. We write m a instead of f a because the m stands for Monad , but monads are just applicative functors that support >>= . The >>= function is pronounced as bind.
When we have a normal value a and a normal function a -> b it's really easy to feed the value to the function you just apply the function to the value normally and that's it. But when we're dealing with values that come with certain contexts, it takes a bit of thinking to see how these fancy values are fed to functions and how to take into account their behavior, but you'll see that it's easy as one two three.
Getting our feet wet with Maybe
Now that we have a vague idea of what monads are about, let's see if we can make that idea a bit less vague.
Much to no one's surprise, Maybe is a monad, so let's explore it a bit more and see if we can combine it with what we know about monads.
Make sure you understand at this point. It's good if you have a feel for how the various Applicative instances work and what kind of computations they represent, because monads are nothing more than taking our existing applicative knowledge and upgrading it.
A value of type Maybe a represents a value of type a with the context of possible failure attached. A value of Just "dharma" means that the string "dharma" is there whereas a value of Nothing represents its absence, or if you look at the string as the result of a computation, it means that the computation has failed.
When we looked at Maybe as a functor, we saw that if we want to fmap a function over it, it gets mapped over the insides if it's a Just value, otherwise the Nothing is kept because there's nothing to map it over!
Like this:
- ghci> fmap (++ "!" ) ( Just "wisdom" )
- Just "wisdom!"
- ghci> fmap (++ "!" ) Nothing
- Nothing
ghci> fmap (++"!") (Just "wisdom")Just "wisdom!"ghci> fmap (++"!") NothingNothing
As an applicative functor, it functions similarly. However, applicatives also have the function wrapped. Maybe is an applicative functor in such a way that when we use <*> to apply a function inside a Maybe to a value that's inside a Maybe , they both have to be Just values for the result to be a Just value, otherwise the result is Nothing . It makes sense because if you're missing either the function or the thing you're applying it to, you can't make something up out of thin air, so you have to propagate the failure:
- ghci> Just (+ ) <*> Just
- Just
- ghci> Nothing <*> Just "greed"
- Nothing
- ghci> Just ord <*> Nothing
- Nothing
ghci> Just (+3) <*> Just 3Just 6ghci> Nothing <*> Just "greed"Nothingghci> Just ord <*> NothingNothing
When we use the applicative style to have normal functions act on Maybe values, it's similar. All the values have to be Just values, otherwise it's all for Nothing !
- ghci> max <$> Just <*> Just
- Just
- ghci> max <$> Just <*> Nothing
- Nothing
ghci> max <$> Just 3 <*> Just 6Just 6ghci> max <$> Just 3 <*> NothingNothing
And now, let's think about how we would do >>= for Maybe . Like we said, >>= takes a monadic value, and a function that takes a normal value and returns a monadic value and manages to apply that function to the monadic value. How does it do that, if the function takes a normal value? Well, to do that, it has to take into account the context of that monadic value.