Prev: Introduction TOC: Contents Next: Doing it with class

Meet the Monads


We will use the Maybe type constructor throughout this chapter, so you should familiarize yourself with the definition and usage of Maybe before continuing.

Type constructors

To understand monads in Haskell, you need to be comfortable dealing with type constructors. A type constructor is a parameterized type definition used with polymorphic types. By supplying a type constructor with one or more concrete types, you can construct a new concrete type in Haskell. In the definition of Maybe:

  data Maybe a = Nothing | Just a
Maybe is a type constructor and Nothing and Just are data constructors. You can construct a data value by applying the Just data constructor to a value:
  country = Just "China"
In the same way, you can construct a type by applying the Maybe type constructor to a type:
  lookupAge :: DB -> String -> Maybe Int

Polymorphic types are like containers that are capable of holding values of many different types. So Maybe Int can be thought of as a Maybe container holding an Int value (or Nothing) and Maybe String would be a Maybe container holding a String value (or Nothing). In Haskell, we can also make the type of the container polymorphic, so we could write "m a" to represent a container of some type holding a value of some type!

We often use type variables with type constructors to describe abstract features of a computation. For example, the polymorphic type Maybe a is the type of all computations that may return a value or Nothing. In this way, we can talk about the properties of the container apart from any details of what the container might hold.

If you get messages about "kind errors" from the compiler when working with monads, it means that you are not using the type constructors correctly.

Maybe a monad

In Haskell a monad is represented as a type constructor (call it m), a function that builds values of that type (a -> m a), and a function that combines values of that type with computations that produce values of that type to produce a new computation for values of that type (m a -> (a -> m b) -> m b). Note that the container is the same, but the type of the contents of the container can change. It is customary to call the monad type constructor "m" when discussing monads in general. The function that builds values of that type is traditionally called "return" and the third function is known as "bind" but is written ">>=". The signatures of the functions are:
-- the type of monad m
data m a = ... 

-- return is a type constructor that creates monad instances 
return :: a -> m a

-- bind is a function that combines a monad instance m a with a computation
-- that produces another monad instance m b from a's to produce a new
-- monad instance m b
(>>=) :: m a -> (a -> m b) -> m b

Roughly speaking, the monad type constructor defines a type of computation, the return function creates primitive values of that computation type and >>= combines computations of that type together to make more complex computations of that type. Using the container analogy, the type constructor m is a container that can hold different values. m a is a container holding a value of type a. The return function puts a value into a monad container. The >>= function takes the value from a monad container and passes it to a function to produce a monad container containing a new value, possibly of a different type. The >>= function is known as "bind" because it binds the value in a monad container to the first argument of a function. By adding logic to the binding function, a monad can implement a specific strategy for combining computations in the monad.

This will all become clearer after the example below, but if you feel particularly confused at this point you might try looking at this physical analogy of a monad before continuing.

An example

Suppose that we are writing a program to keep track of sheep cloning experiments. We would certainly want to know the genetic history of all of our sheep, so we would need mother and father functions. But since these are cloned sheep, they may not always have both a mother and a father!

We would represent the possibility of not having a mother or father using the Maybe type constructor in our Haskell code:
type Sheep = ...

father :: Sheep -> Maybe Sheep
father = ...

mother :: Sheep -> Maybe Sheep
mother = ...

Then, defining functions to find grandparents is a little more complicated, because we have to handle the possibility of not having a parent:
maternalGrandfather :: Sheep -> Maybe Sheep
maternalGrandfather s = case (mother s) of
                          Nothing -> Nothing
                          Just m  -> father m
and so on for the other grandparent combinations.

It gets even worse if we want to find great grandparents:
mothersPaternalGrandfather :: Sheep -> Maybe Sheep
mothersPaternalGrandfather s = case (mother s) of
                                 Nothing -> Nothing
                                 Just m  -> case (father m) of
                                              Nothing -> Nothing
                                              Just gf -> father gf

Aside from being ugly, unclear, and difficult to maintain, this is just too much work. It is clear that a Nothing value at any point in the computation will cause Nothing to be the final result, and it would be much nicer to implement this notion once in a single place and remove all of the explicit case testing scattered all over the code. This will make the code easier to write, easier to read and easier to change. So good programming style would have us create a combinator that captures the behavior we want:
Code available in example1.hs
-- comb is a combinator for sequencing operations that return Maybe
comb :: Maybe a -> (a -> Maybe b) -> Maybe b
comb Nothing  _ = Nothing
comb (Just x) f = f x

-- now we can use `comb` to build complicated sequences
mothersPaternalGrandfather :: Sheep -> Maybe Sheep
mothersPaternalGrandfather s = (Just s) `comb` mother `comb` father `comb` father 

The combinator is a huge success! The code is much cleaner and easier to write, understand and modify. Notice also that the comb function is entirely polymorphic — it is not specialized for Sheep in any way. In fact, the combinator captures a general strategy for combining computations that may fail to return a value. Thus, we can apply the same combinator to other computations that may fail to return a value, such as database queries or dictionary lookups.

The happy outcome is that common sense programming practice has led us to create a monad without even realizing it. The Maybe type constructor along with the Just function (acts like return) and our combinator (acts like >>=) together form a simple monad for building computations which may not return a value. All that remains to make this monad truly useful is to make it conform to the monad framework built into the Haskell language. That is the subject of the next chapter.

A list is also a monad

We have seen that the Maybe type constructor is a monad for building computations which may fail to return a value. You may be surprised to know that another common Haskell type constructor, [] (for building lists), is also a monad. The List monad allows us to build computations which can return 0, 1, or more values.

The return function for lists simply creates a singleton list (return x = [x]). The binding operation for lists creates a new list containing the results of applying the function to all of the values in the original list (l >>= f = concatMap f l).

One use of functions which return lists is to represent ambiguous computations — that is computations which may have 0, 1, or more allowed outcomes. In a computation composed from ambigous subcomputations, the ambiguity may compound, or it may eventually resolve into a single allowed outcome or no allowed outcome at all. During this process, the set of possible computational states is represented as a list. The List monad thus embodies a strategy for performing simultaneous computations along all allowed paths of an ambiguous computation.

Examples of this use of the List monad, and contrasting examples using the Maybe monad will be presented shortly. But first, we must see how useful monads are defined in Haskell.

Summary

We have seen that a monad is a type constructor, a function called return, and a combinator function called bind or >>=. These three elements work together to encapsulate a strategy for combining computations to produce more complex computations.

Using the Maybe type constructor, we saw how good programming practice led us to define a simple monad that could be used to build complex computations out of sequences of computations that could each fail to return a value. The resulting Maybe monad encapsulates a strategy for combining computations that may not return values. By codifying the strategy in a monad, we have achieved a degree of modularity and flexibility that is not present when the computations are combined in an ad hoc manner.

We have also seen that another common Haskell type constructor, [], is a monad. The List monad encapsulates a strategy for combining computations that can return 0, 1, or multiple values.


Prev: Introduction TOC: Contents Next: Doing it with class