Representing Failure
We have already looked at our find
function for lists:
>>> find even [1,2,3,4,5]
Just 2
>>> find even [1,3,5]
Nothing
find pred xs
returns Just
the first element in xs
that satisfies pred
or
Nothing
if it fails to find such an element. So that's our representation of
success or failure: Just x
is what a function returns when it succeeds and the
result is x
. If the function fails, it returns Nothing
.
Now how about the following problem? We are given a list that stores a bunch of students along with their banner numbers:
>>> bannerNumbers = [("John Doe", 1234), ("Jane Doe", 1235), ("Teacher's Pet", 1), ("Sleepy Head", 555)]
and a list that stores each student's grade in some course, keyed by their banner numbers:
>>> grades = [(1, "A+"), (555, "C"), (1234, "B")]
We want to implement a function that allows us to look up a student's grade by
name. The process is rather obvious: We find the banner number in the
bannerNumbers
list and then look up the grade by banner number in grades
.
First we need a function that gives us the banner number corresponding to the student's name:
bannerNumberByName :: String -> Maybe Int
bannerNumberByName name = snd <$> find (\entry -> fst entry == name) bannerNumbers
Remember, fst
gives the first component of a pair, snd
gives the second
component of a pair, and (<$>)
is just the infix version of fmap
. For the
Maybe
functor, fmap
applies the given function to x
if the Maybe
value
is Just x
. If it is Nothing
, then fmap
ping any function over it gives
Nothing
again. So what this function does is to return Just
the banner
number corresponding to the given name if there exists such a banner number, and
Nothing
if there is no banner number associated with the given name:
>>> bannerNumberByName name = snd <$> find (\entry -> fst entry == name) bannerNumbers
>>> bannerNumberByName "Jane Doe"
Just 1235
>>> bannerNumberByName "Johnny Doe"
Nothing
Similarly, we can look up the grade by banner number:
gradeByBannerNumber :: Int -> Maybe String
gradeByBannerNumber banner = snd <$> find (\entry -> fst entry == banner) grades
>>> gradeByBannerNumber banner = snd <$> find (\entry -> fst entry == banner) grades
>>> gradeByBannerNumber 555
Just "C"
>>> gradeByBannerNumber 556
Nothing
The logic of this function is exactly the same as that of bannerNumberByName
.
What we want to implement now is a function
gradeByName :: String -> Maybe String
Ideally, we'd like to implement it as
gradeByName name = gradeByBannerNumber (bannerNumberByName name)
but this doesn't work because gradeByBannerNumber
expects an Int
argument,
and bannerNumberByName
returns Maybe Int
. And this composition shouldn't
work, because both bannerNumberByName
and gradeByBannerNumber
may fail. What
should we apply gradeByBannerNumber
to if bannerNumberByName name
returns
Nothing
? Similarly, we may be able to find a banner number for name
, but
then the lookup of banner number in grades
comes up empty. Again, we have no
choice but to report Nothing
, because we didn't find a grade associated with
the given name
. So the logic is that if either bannerNumberByName
or
gradeByBannerNumber
comes up empty, then the whole function must return
Nothing
. We can easily implement this using a careful case analysis:
gradeByName name = case bannerNumberByName name of
Nothing -> Nothing
Just banner -> gradeByBannerNumber banner
You can verify that this does what we expect:
>>> :{
| gradeByName name = case bannerNumberByName name of
| Nothing -> Nothing
| Just banner -> gradeByBannerNumber banner
| :}
>>> gradeByName "John Doe"
Just "B"
>>> gradeByName "Johnny Doe"
Nothing
>>> gradeByName "Jane Doe"
Nothing
The grade lookup for "John Doe" succeeded. The other two lookups failed for
different reasons. The grade lookup for "Johnny Doe" failed because there is no
Johnny Doe in bannerNumbers
. So bannerNumberByName
returns Nothing
, and we
use the first branch of the case
expression, which returns Nothing
. There
does exist a banner number for "Jane Doe" in bannerNumbers
, so
bannerNumberByName
returns Just
Jane Doe's banner
number. However, the
grade lookup using gradeByBannerNumber banner
fails because there is no grade
recorded in grades
for Jane Doe's banner number, 1235.
There's a more elegant way to implement gradeByName
using the maybe
function
from the standard library:
maybe :: b -> (a -> b) -> Maybe a -> b
maybe def f x = case x of
Nothing -> def
Just y -> f y
maybe def f x
applies f
to the value y
if x
contains a value, is
Just y
. If x
does not contain any value to which to apply f
, that is, if
x = Nothing
, then maybe def f x
returns the default value def
. With the
help of maybe
, we can implement gradeByname
more elegantly:
gradeByName = maybe Nothing gradeByBannerNumber . bannerNumberByName
>>> gradeByName = maybe Nothing gradeByBannerNumber . bannerNumberByName
>>> gradeByName "John Doe"
Just "B"
>>> gradeByName "Johnny Doe"
Nothing
>>> gradeByName "Jane Doe"
Nothing
Still, it's tedious to compose functions that can fail in this fashion,
especially if we have more than two functions to compose. We would like to
extract the common pattern that a computation composed of multiple steps that
can fail can itself fail, and that the computation aborts as soon as it
encounters the first failure. To this end, we turn Maybe
into a monad.