Monads
CIS 194 Week 12
8 April 2013
Suggested reading:
- The Typeclassopedia
- LYAH Chapter 12: A Fistful of Monads
- LYAH Chapter 9: Input and Output
- RWH Chapter 7: I/O
- RWH Chapter 14: Monads
- RWH Chapter 15: Programming with monads
Motivation
Over the last couple of weeks, we have seen how the Applicative
class allows us to idiomatically handle computations which take place in some sort of “special context”—for example, taking into account possible failure with Maybe
, multiple possible outputs with []
, consulting some sort of environment using ((->) e)
, or construct parsers using a “combinator” approach, as in the homework.
However, so far we have only seen computations with a fixed structure, such as applying a data constructor to a fixed set of arguments. What if we don’t know the structure of the computation in advance – that is, we want to be able to decide what to do based on some intermediate results?
As an example, recall the Parser
type from the homework, and assume that we have implemented Functor
and Applicative
instances for it:
newtype Parser a = Parser { runParser :: String -> Maybe (a, String) }
instance Functor Parser where
...
instance Applicative Parser where
...
Recall that a value of type Parser a
represents a parser which can take a String
as input and possibly produce a value of type a
, along with the remaining unparsed portion of the String
. For example, a parser for integers, given as input the string
"143xkkj"
might produce as output
Just (143, "xkkj")
As you saw in the homework, we can now write things like
data Foo = Bar Int Int Char
parseFoo :: Parser Foo
parseFoo = Bar <$> parseInt <*> parseInt <*> parseChar
assuming we have functions parseInt :: Parser Int
and parseChar :: Parser Char
. The Applicative
instance automatically handles the possible failure (if parsing any of the components fail, parsing the entire Foo
will fail) and threading through the unconsumed portion of the String
input to each component in turn.
However, suppose we are trying to parse a file containing a sequence of numbers, like this:
4 78 19 3 44 3 1 7 5 2 3 2
The catch is that the first number in the file tells us the length of a following “group” of numbers; the next number after the group is the length of the next group, and so on. So the example above could be broken up into groups like this:
78 19 3 44 -- first group
1 7 5 -- second group
3 2 -- third group
This is a somewhat contrived example, but in fact there are many “real-world” file formats that follow a similar principle—you read some sort of header which then tells you the lengths of some following blocks, or where to find things in the file, and so on.
We would like to write a parser for this file format of type
parseFile :: Parser [[Int]]
Unfortunately, this is not possible using only the Applicative
interface. The problem is that Applicative
gives us no way to decide what to do next based on previous results: we must decide in advance what parsing operations we are going to run, before we see the results.
It turns out, however, that the Parser
type can support this sort of pattern, which is abstracted into the Monad
type class.
Monad
The Monad
type class is defined as follows:
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
m1 >> m2 = m1 >>= \_ -> m2
This should look familiar! We have seen these methods before in the context of IO
, but in fact they are not specific to IO
at all. It’s just that a monadic interface to IO
has proved useful.
return
also looks familiar because it has the same type as pure
. In fact, every Monad
should also be an Applicative
, with pure = return
. The reason we have both is that Applicative
was invented after Monad
had already been around for a while.
(>>)
is just a specialized version of (>>=)
(it is included in the Monad
class in case some instance wants to provide a more efficient implementation, but usually the default implementation is just fine). So to understand it we first need to understand (>>=)
.
There is actually a fourth method called fail
, but putting it in the Monad
class was a mistake, and you should never use it, so I won’t tell you about it (you can read about it in the Typeclassopedia if you are interested).
(>>=)
(pronounced “bind”) is where all the action is! Let’s think carefully about its type:
(>>=) :: m a -> (a -> m b) -> m b
(>>=)
takes two arguments. The first one is a value of type m a
. (Incidentally, such values are sometimes called monadic values, or computations. It has also been proposed to call them mobits. The one thing you must not call them is “monads”, since that is a kind error: the type constructor m
is a monad.) In any case, the idea is that a mobit of type m a
represents a computation which results in a value (or several values, or no values) of type a
, and may also have some sort of “effect”:
c1 :: Maybe a
is a computation which might fail but results in ana
if it succeeds.c2 :: [a]
is a computation which results in (multiple)a
s.c3 :: Parser a
is a computation which implicitly consumes part of aString
and (possibly) produces ana
.c4 :: IO a
is a computation which potentially has some I/O effects and then produces ana
.
And so on. Now, what about the second argument to (>>=)
? It is a function of type (a -> m b)
. That is, it is a function which will choose the next computation to run based on the result(s) of the first computation. This is precisely what embodies the promised power of Monad
to encapsulate computations which can choose what to do next based on the results of previous computations.
So all (>>=)
really does is put together two mobits to produce a larger one, which first runs one and then the other, returning the result of the second one. The all-important twist is that we get to decide which mobit to run second based on the output from the first.
The default implementation of (>>)
should make sense now:
(>>) :: m a -> m b -> m b
m1 >> m2 = m1 >>= \_ -> m2
m1 >> m2
simply does m1
and then m2
, ignoring the result of m1
.
Examples
Let’s start by writing a Monad
instance for Maybe
:
instance Monad Maybe where
return = Just
Nothing >>= _ = Nothing
Just x >>= k = k x
return
, of course, is Just
. If the first argument of (>>=)
is Nothing
, then the whole computation fails; otherwise, if it is Just x
, we apply the second argument to x
to decide what to do next.
Incidentally, it is common to use the letter k
for the second argument of (>>=)
because k
stands for “continuation”. I wish I was joking.
Some examples:
check :: Int -> Maybe Int
check n | n < 10 = Just n
| otherwise = Nothing
halve :: Int -> Maybe Int
halve n | even n = Just $ n `div` 2
| otherwise = Nothing
ex01 = return 7 >>= check >>= halve
ex02 = return 12 >>= check >>= halve
ex03 = return 12 >>= halve >>= check
How about a Monad
instance for the list constructor []
?
instance Monad [] where
return x = [x]
xs >>= k = concat (map k xs)
A simple example:
addOneOrTwo :: Int -> [Int]
addOneOrTwo x = [x+1, x+2]
ex04 = [10,20,30] >>= addOneOrTwo
Monad combinators
One nice thing about the Monad
class is that using only return
and (>>=)
we can build up a lot of nice general combinators for programming with monads. Let’s look at a couple.
First, sequence
takes a list of monadic values and produces a single monadic value which collects the results. What this means depends on the particular monad. For example, in the case of Maybe
it means that the entire computation succeeds only if all the individual ones do; in the case of IO
it means to run all the computations in sequence; in the case of Parser
it means to run all the parsers on sequential parts of the input (and succeed only if they all do).
sequence :: Monad m => [m a] -> m [a]
sequence [] = return []
sequence (ma:mas) =
ma >>= \a ->
sequence mas >>= \as ->
return (a:as)
Using sequence
we can also write other combinators, such as
replicateM :: Monad m => Int -> m a -> m [a]
replicateM n m = sequence (replicate n m)
And now we are finally in a position to write the parser we wanted to write: it is simply
parseFile :: Parser [[Int]]
parseFile = many parseLine
parseLine :: Parser [Int]
parseLine = parseInt >>= \i -> replicateM i parseInt
(many
was also known as zeroOrMore
on the homework).
Generated 2013-04-04 15:32:20.090162