Skip to content

Maybe as a Monad

Let's remind ourselves what a monad is: It's a functor m, that is, a container. And this container must come equipped with two functions. return takes a value and produces a container storing this value: return :: a -> m a. Given a container x :: m a and a function f :: a -> m b, x >>= f needs to apply f to every element in x and combine the containers of type m b produced by all these applications of f into a single container of type m b.

Here, our container is Maybe. A container that stores some value x is Just x. Thus,

return x = Just x

or, more succinctly,

return = Just

In the expression x >>= f, we want to view x and f as steps in a computation. Both steps can fail. The fact that x can fail is represented by x having the type Maybe a; x may produce a value of type a (Just y), or it may fail to do so (Nothing). f can be seen as continuing the computation using the value y produced by x. If x fails, then f has nothing to work with, so we have no choice but to abort, and the whole computation x >>= f fails, is Nothing. If x succeeds, then f does whatever it does with the value y produced by x. f may itself fail. Hence, its return type is Maybe b. The whole computation x >>= f is still a failure if f fails, as in our example where looking up Jane Doe's banner number succeeded—that's x—but looking up a grade associated with this banner number failed—that's f. If f succeeds, then the result is whatever result f produces.

This is exactly the same logic that we used to implement gradyByName in the previous subsection, only we replaced the two steps bannerNumberByName and gradeByBannerNumber with two generic steps x and f. Thus, we obtain

x >>= f = maybe Nothing f x

Putting it all together, here is our Monad instance for Maybe:

instance Monad Maybe where
    return  = Just
    x >>= f = maybe Nothing f x

With this, we can define

gradeByName name = bannerNumberByName name >>= gradeByBannerNumber

or, more succinctly,

gradeByName = bannerNumberByName >=> gradeByBannerNumber

Remember, (>=>) works for any monad. We implemented it as

(f >=> g) x = f x >>= g

With f = bannerNumberByName and g = gradeByBannerNumber, we have

gradeByName name = bannerNumberByName name >>= gradeByBannerNumber      -- Definition of gradeByName
                 = (bannerNumberByName >=> gradeByBannerNumber) name    -- Definition of (>=>)
gradeByName      =  bannerNumberByName >=> gradeByBannerNumber          -- Drop common argument name from
                                                                        -- both sides

Let's try it out. To use (>=>), we need to import the Control.Monad module.

GHCi
>>> import Control.Monad
>>> gradeByName = bannerNumberByName >=> gradeByBannerNumber
>>> gradeByName "John Doe"
Just "B"
>>> gradeByName "Johnny Doe"
Nothing
>>> gradeByName "Jane Doe"
Nothing

To make double-sure that this definition of a Monad instance for Maybe is sound, let's verify that it satisfies the monad laws. (We'll do this for Maybe and for the list monad, and I'll leave it as an exercise for you for the other monads we discuss.) Since we formulated the monad laws in terms of composition of decorated functions,1 we need to use the implementation of (>=>) derived from (>>=) shown above.

So let's verify the laws:

  • Identity: The identity law says that

    return >=> f = f = f >=> return
    

    Let's verify both equalities separately:

    (return >=> f) x
        = return x >>= f                -- Definition of (>=>)
        = maybe Nothing f (return x)    -- Definition of (>>=)
        = maybe Nothing f (Just x)      -- Definition of return
        = case Just x of                -- Definition of maybe
              Nothing -> Nothing
              Just y  -> f y
        = f x                           -- Pattern matching
    return >=> f = f                    -- Drop the common argument x on both sides
    

    and

    (f >=> return) x
        = f x >>= return                -- Definition of (>=>)
        = maybe Nothing return (f x)    -- Definition of (>>=)
        = maybe Nothing Just (f x)      -- Definition of return
        = case f x of                   -- Definition of maybe
              Nothing -> Nothing
              Just y  -> Just y
        = f x                           -- Pattern matching
    f >=> return = f                    -- Drop the common argument x on both sides
    
  • Associativity: The associativity law says that

    (f >=> g) >=> h = f >=> (g >=> h)
    

    With our definition of f >=> g above, we obtain

    ((f >=> g) >=> h) x
        = (f >=> g) x >>= h                          -- Definition of (>=>)
        = (f x >>= g) >>= h                          -- Definition of (>=>)
        = maybe Nothing h (f x >>= g)                -- Definition of (>>=)
        = maybe Nothing h (maybe Nothing g (f x))    -- Definition of (>>=)
        = case f x of                                -- Definition of maybe and
              Nothing -> maybe Nothing h Nothing     -- inlining of "maybe Nothing h"
              Just y  -> maybe Nothing h (g y)
        = case f x of
              Nothing -> case Nothing of             -- Definition of maybe
                  Nothing -> Nothing
                  Just z  -> h z
              Just y  -> g y >>= h                   -- Definition of (>>=)
        = case f x of
              Nothing -> Nothing                     -- Pattern matching
              Just y  -> (g >=> h) y                 -- Definition of (>=>)
        = maybe Nothing (g >=> h) (f x)              -- Definition of maybe
        = f x >>= (g >=> h)                          -- Definition of (>>=)
        = (f >=> (g >=> h)) x                        -- Definition of (>=>)
    (f >=> g) >=> h = f >=> (g >=> h)                -- Drop the common argument x on both sides
    

This was a lot of acrobatics to arrive at our definition of return and (>>=) for the Maybe monad and to verify that these definitions do satisfy the monad laws, but we've gained a lot: Just as we can compose normal functions using standard function composition, as in k . h . g . f, we can now easily compose functions that may fail, as in f >=> g >=> h >=> k, and the whole computation automatically aborts and fails as soon as one of the steps in this pipeline fails. There's no need to check the return value of each individual function application. The implementation of (>>=) does that for us.

We would like to do even better and avoid having to generously sprinkle (>=>) operators throughout our code. do-notation let's us do this. We'll discuss it shortly, after figuring out how to catch exceptions as we would in Java.


  1. One can also define the monad laws directly in terms of (>>=) or in terms of join. If you look at the documentation of the Monad class on Hoogle, you'll find the following laws in terms of (>>=):

    • Left identity: return x >>= f = f x
    • Right identity: f >>= return = f
    • Associativity: f >>= (\x -> g x >>= h) = (f >>= g) >>= h

    I think we can agree that it is much harder to see why these laws are called identity and associativity laws, whereas their translations into laws for (>=>) make this really obvious.

    For completeness, the monad laws in terms of join are

    • Identity: join . return = id = join . fmap return
    • Associativity: join . fmap join = join . join

    This is probably even more obscure, even though it captures exactly the monad laws used in category theory, and all of these laws are equivalent to the ones we stated for (>=>)