Learn You a Haskell for Great Good! - Miran Lipovaca [149]
liftA2 is a convenience function for applying a function between two applicative values. It’s defined like so:
liftA2 :: (Applicative f) => (a -> b -> c) -> f a -> f b -> f c
liftA2 f x y = f <$> x <*> y
The liftM2 function does the same thing, but with a Monad constraint. There are also liftM3, liftM4, and liftM5 functions.
You saw how monads are at least as strong as applicatives and functors and how even though all monads are functors and applicative functors, they don’t necessarily have Functor and Applicative instances. We examined the monadic equivalents of the functions that functors and applicative functors use.
The join Function
Here’s some food for thought: If the result of one monadic value is another monadic value (one monadic value is nested inside the other), can you flatten them to just a single, normal monadic value? For instance, if we have Just (Just 9), can we make that into Just 9? It turns out that any nested monadic value can be flattened and that this is actually a property unique to monads. For this, we have the join function. Its type is this:
join :: (Monad m) => m (m a) -> m a
So, join takes a monadic value within a monadic value and gives us just a monadic value—it flattens it, in other words. Here it is with some Maybe values:
ghci> join (Just (Just 9))
Just 9
ghci> join (Just Nothing)
Nothing ghci> join Nothing
Nothing
The first line has a successful computation as a result of a successful computation, so they are both just joined into one big successful computation. The second line features a Nothing as a result of a Just value. Whenever we were dealing with Maybe values before and we wanted to combine several of them into one—be it with <*> or >>=—they all needed to be Just values for the result to be a Just value. If there was any failure along the way, the result was a failure, and the same thing happens here. In the third line, we try to flatten what is from the onset a failure, so the result is a failure as well.
Flattening lists is pretty intuitive:
ghci> join [[1,2,3],[4,5,6]]
[1,2,3,4,5,6]
As you can see, for lists, join is just concat. To flatten a Writer value whose result is a Writer value itself, we need to mappend the monoid value:
ghci> runWriter $ join (Writer (Writer (1, "aaa"), "bbb"))
(1,"bbbaaa")
The outer monoid value "bbb" comes first, and then "aaa" is appended to it. Intuitively speaking, when you want to examine the result of a Writer value, you need to write its monoid value to the log first, and only then can you look at what it has inside.
Flattening Either values is very similar to flattening Maybe values:
ghci> join (Right (Right 9)) :: Either String Int
Right 9
ghci> join (Right (Left "error")) :: Either String Int
Left "error"
ghci> join (Left "error") :: Either String Int
Left "error"
If we apply join to a stateful computation whose result is a stateful computation, the result is a stateful computation that first runs the outer stateful computation and then the resulting one. Watch it at work:
ghci> runState (join (state $ \s -> (push 10, 1:2:s))) [0,0,0]
((),[10,1,2,0,0,0])
The lambda here takes a state, puts 2 and 1 onto the stack, and presents push 10 as its result. So, when this whole thing is flattened with join and then run, it first puts 2 and 1 onto the stack, and then push 10 is carried out, pushing a 10 onto the top.
The implementation for join is as follows:
join :: (Monad m) => m (m a) -> m a
join mm = do
m <- mm
m
Because the result of mm is a monadic value, we get that result and then just put it on a line of its own because it’s a monadic value. The trick here is that when we call m <- mm, the context of the monad that we are in is taken care of. That’s why, for instance, Maybe values result in Just values only if the outer and inner values are both Just values. Here’s what this would look like if the mm value were set in advance to Just