| Prev: Introduction | TOC: Contents | Next: Doing it with class |
We will use the Maybe type constructor
throughout this chapter, so you should familiarize yourself with the
definition and usage of
Maybe before continuing.
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.
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.
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
|
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.
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.
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 |