Maybe Monad is Not So Scary

We have seen several ways that, by carefully classifying expressions with different types, Haskell is able to provide the power to write functions that work in many settings (avoiding a lot of boilerplate code) while statically guaranteeing that a large class of run-time errors will never occur. Now, we will start learning about how Haskell carefully classifies types into different, well, types in order to push the expressive power even further, to identify common patterns of computation.

To work up to this, let's do a bit of programming. Recall the definition of the Maybe type, which can be used in many cases to encode either "failure" or a "successful" result. We will see examples of:

  1. mapping Maybe values,
  2. applying Maybe function values, and
  3. sequencing Maybe actions.

Mapping Maybe Values

We can write a couple example functions that "map over" Maybe values as routine recursive definitions.

addOne :: Num a => Maybe a -> Maybe a
addOne Nothing  = Nothing
addOne (Just n) = Just $ 1 + n

square :: Num a => Maybe a -> Maybe a
square Nothing  = Nothing
square (Just n) = Just $ n * n

maybeLength :: Maybe [a] -> Maybe Int
maybeLength Nothing   = Nothing
maybeLength (Just xs) = Just $ length xs

maybeShow :: Maybe a -> Maybe String
maybeShow Nothing  = Nothing
maybeShow (Just x) = Just $ show x

As usual when seeing repeated structure in our code, we look for ways to streamline, in this case by factoring out the mapping function.

mapMaybe :: (a -> b) -> Maybe a -> Maybe b
mapMaybe f Nothing  = Nothing
mapMaybe f (Just x) = Just $ f x

addOne = mapMaybe (1+)
square = mapMaybe (^2)
maybeShow = mapMaybe show

The type and expression structure of mapMaybe should look familiar. Recall our old friend map that "maps over" lists.

map :: (a -> b) -> [a] -> [b]
map f []     = []
map f (x:xs) = f x : map f xs

Very many types arise in programming where having this kind of general map function is useful. So, treating them uniformly in the language will reap benefits.

Applying Maybe Functions

mapMaybe works well for functions with one argument...

mapMaybe (+2) (Just 10)           -- Num a => Maybe a

... but doesn't help with more:

mapMaybe (+) (Just 10)            -- Num a => Maybe (a -> a)
mapMaybe (+) (Just 10) (Just 2)   -- type error

We also want to be able to apply Maybe functions.

liftMaybe                     :: a -> Maybe a
liftMaybe                     =  Just

applyMaybe                    :: Maybe (a -> b) -> Maybe a -> Maybe b
applyMaybe (Just g) (Just x)  =  Just $ g x
applyMaybe _        _         =  Nothing

Now we can handle multi-arg Maybe functions:

> liftMaybe (+) `applyMaybe` Just 10                       -- Num a => Maybe (a -> a)
> liftMaybe (+) `applyMaybe` Just 10 `applyMaybe` Just 2   -- Num a => Maybe a

> liftMaybe (+) `applyMaybe` Nothing `applyMaybe` Just 2
> Nothing `applyMaybe` Nothing `applyMaybe` Just 2

We can write helper functions to "lift" pure functions with different arities. For example:

lift3Maybe :: (a -> b -> c -> d) -> Maybe a -> Maybe b -> Maybe c -> Maybe d
lift3Maybe f ma mb mc =
  liftMaybe f `applyMaybe` ma `applyMaybe` mb `applyMaybe` mc

Notice that, because

mapMaybe   :: {- forall t1, t2. -} (t1 -> t2) -> Maybe t1 -> Maybe t2
mapMaybe f :: Maybe a -> Maybe (b -> c -> d)

we can call mapMaybe f in place of applyMaybe (liftMaybe f).

lift3Maybe f ma mb mc =
  f `mapMaybe` ma `applyMaybe` mb `applyMaybe` mc

Infix Operators for Applicative Style

It is a common pattern to apply a pure function of arity n to n Maybe arguments. To make this common case look as close to function application as possible, we can define two helper infix operators.

(<$>) = mapMaybe
(<*>) = applyMaybe

Now, the arity-n pattern above can be written in applicative style:

lift2Maybe f ma mb =
  f <$> ma <*> mb

lift3Maybe f ma mb mc =
  f <$> ma <*> mb <*> mc

lift4Maybe f ma mb mc md =
  f <$> ma <*> mb <*> mc <*> md

For example:

> mapMaybe (+) (Just 1) <*> Just 2
> (+) <$> Just 1 <*> Just 2

Sequencing Maybe Actions

Motivating example:

type Person = String

father :: Person -> Maybe Person
father = undefined   -- assuming this is defined in some reasonable way

grandfather :: Person -> Maybe Person
grandfather p =
  case father p of
    Nothing -> Nothing
    Just fp ->
      case father p of
        Nothing  -> Nothing
        Just ffp -> Just ffp

Can avoid the second case expression...

grandfather :: Person -> Maybe Person
grandfather p =
  case father p of
    Nothing -> Nothing
    Just fp -> father p

but still, it is tedious to manipulate Nothing and Just values explicitly. However, neither mapMaybe nor applyMaybe can help here; the second function call, father p, returns Nothing or Just some value depending on the value of p.

applyIfMaybe :: (a -> Maybe b) -> Maybe a -> Maybe b
applyIfMaybe _ Nothing  = Nothing
applyIfMaybe f (Just x) = f x

Now we can avoid manipulating Nothing explicitly:

grandfather p =
  father `applyIfMaybe` (father `applyIfMaybe` (Just p))

If we flip the arguments to applyIfMaybe

andThenMaybe :: Maybe a -> (a -> Maybe b) -> Maybe b
andThenMaybe = flip applyIfMaybe

then it looks more like sequencing the function calls one after another from left-to-right:

grandfather p =
  father p `andThenMaybe` (\fp ->
    father fp `andThenMaybe` (\ffp ->
      Just ffp
  ))

We can eta-reduce the innermost lambda:

grandfather p =
  father p `andThenMaybe` (\fp -> father fp `andThenMaybe` Just)

And, based on the definition of andThenMaybe:

grandfather p =
  father p `andThenMaybe` father

This pattern of function composition can be abstracted:

(<=<) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c)
(f <=< g) x = g x `andThenMaybe` f

And now:

grandfather = father <=< father

Infix Operator for Sequencing

By defining a right-associative infix operator defined to be andThenMaybe

(>>=) = andThenMaybe

we can write sequences of Maybe actions without extra parentheses. For example:

grandfather p =
  father p   >>= \fp ->
  father fp  >>= \ffp ->
    Just ffp

Errors Within a Sequence of Actions

sibling :: Person -> Person -> Bool
sibling x y =
  case (father x, father y) of
    (Just fx, Just fy) -> fx == fy && x /= y
    _                  -> False

There are a couple aspects that are undesirable. The first is that we should perform the x /= y comparison before forcing either father x or father y to evaluate.

sibling x y =
  x /= y && sameParent where
    sameParent =
      case (father x, father y) of
        (Just fx, Just fy) -> fx == fy && x /= y
        _                  -> False

The other is that we have to manually pattern match to get to the interesting, non-error case. If we encode Bools using Maybe

guardMaybe :: Bool -> Maybe ()
guardMaybe True  = Just ()
guardMaybe False = Nothing

then we can reuse the Maybe sequencing:

sibling :: Person -> Person -> Maybe ()
  (guardMaybe $ x /= y) >>= \() ->
  father x              >>= \fx ->
  father y              >>= \fy ->
  guardMaybe $ fx == fy

Notice that we changed the type of sibling, but it's trivial to convert from Maybe () back to Bool.

Recap

We've seen a bunch of useful functions for manipulating Maybe values.

liftMaybe         :: a -> Maybe a

mapMaybe          ::       (a -> b)       -> (Maybe a -> Maybe b)
applyMaybe        :: Maybe (a -> b)       -> (Maybe a -> Maybe b)
flip andThenMaybe ::       (a -> Maybe b) -> (Maybe a -> Maybe b)

andThenMaybe      :: Maybe a -> (a -> Maybe b) -> Maybe b

guardMaybe        :: Bool -> Maybe ()

For each of the above, we can replace Maybe with many other different types t and implement suitable definitions with analogous type signatures and behavior:

lift         :: a -> t a                       -- Applicative_

map          ::   (a -> b)   -> (t a -> t b)   -- Functor_
apply        :: t (a -> b)   -> (t a -> t b)   -- Applicative_
flip andThen ::   (a -> t b) -> (t a -> t b)   -- Monad_

andThen      :: t a -> (a -> t b) -> t b       -- Monad_

guard        :: Bool -> t ()                   -- MonadPlus_

In the next several sections, we'll see how the Haskell type class system describes these, and other, useful patterns of computation.

Source Files

results matching ""

    No results matching ""