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:
- mapping
Maybe
values, - applying
Maybe
function values, and - 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 Bool
s 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.