We introduced monads using
Maybe as an example. The
Maybe monad represents computations which might "go wrong", in the sense of not returning a value; the definitions of
Monad amounting to:
return :: a -> Maybe a return x = Just x (>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b (>>=) m g = case m of Nothing -> Nothing Just x -> g x
Now, we will present two additional examples, similar in spirit to the grandparents one in the previous chapter, and then conclude with some general points.
Maybe datatype provides a way to make a safety wrapper around functions which can fail to work for a range of arguments.
tail, which only work with non-empty lists, would be a good example of that. Another typical case, which we will explore in this section, are mathematical functions like
log, which (as far as real numbers are concerned) are only defined for non-negative arguments. For instance, our implementation of a "safe" square root function could be:
safeSqrt :: (Floating a, Ord a) => a -> Maybe a safeSqrt x | x >= 0 = Just (sqrt x) | otherwise = Nothing
We could now decide to write similar "safe functions" for all functions with limited domains, such as division, logarithm and inverse trigonometric functions (
safeArcSin, etc.). However, when we try to combine them we run into a problem: they output a
Maybe a, but take an
a as an input. As a result, before each application, we have to check whether the previous operation was successful:
safeLogSqrt :: (Floating a, Ord a) => a -> Maybe a safeLogSqrt x = case safeSqrt x of Just root -> safeLog root Nothing -> Nothing
You can see the problem: the code looks ugly already when using one concatenation. If you had multiple concatenations the code would get even worse. This is in stark contrast to the easy concatenation that we get with "unsafe" functions:
unsafeLogSqrt = log . sqrt
This, however, is precisely the sort of issue monads can tackle: through
(>>=), they allow us to concatenate computations easily, so that the result of one is fed to the next.
Maybe being a monad, we can achieve the desired effect very simply:
> return 1000 >>= safeSqrt >>= safeLog Just 3.4538776394910684 > return (-1000) >>= safeSqrt >>= safeLog Nothing
safeLogSqrt :: (Floating a, Ord a) => a -> Maybe a safeLogSqrt x = return x >>= safeSqrt >>= safeLog
Every additional operation is just another
>>= safeOperation term—no checks required, since
(>>=) takes care of that. To make things even easier, we can write the combination in a style which mirrors composition of "unsafe" functions with
(.) by using the
(<=<) operator from the
import Control.Monad((<=<)) safeLogSqrt' = safeLog <=< safeSqrt
A lookup table is a table which relates keys to values. You look up a value by knowing its key and using the lookup table. For example, you might have a lookup table of contact names as keys to their phone numbers as the values in a phone book application. An elementary way of implementing lookup tables in Haskell is to use a list of pairs:
[(a, b)]. Here
a is the type of the keys, and
b the type of the values. Here's how the phone book lookup table might look like:
phonebook :: [(String, String)] phonebook = [ ("Bob", "01788 665242"), ("Fred", "01624 556442"), ("Alice", "01889 985333"), ("Jane", "01732 187565") ]
The most common thing you might do with a lookup table is look up values! However, this computation might fail. Everything is fine if we try to look up one of "Bob", "Fred", "Alice" or "Jane" in our phone book, but what if we were to look up "Zoe"? Zoe isn't in our phone book, so the lookup would fail. Hence, the Haskell function to look up a value from the table is a
Maybe computation (it is available from Prelude):
lookup :: Eq a => a -- a key -> [(a, b)] -- the lookup table to use -> Maybe b -- the result of the lookup
Let us explore some of the results from lookup:
Prelude> lookup "Bob" phonebook Just "01788 665242" Prelude> lookup "Jane" phonebook Just "01732 187565" Prelude> lookup "Zoe" phonebook Nothing
Now let's expand this into using the full power of the monadic interface. Say, we're now working for the government, and once we have a phone number from our contact, we want to look up this phone number in a big, government-sized lookup table to find out the registration number of their car. This, of course, will be another
Maybe-computation. But if they're not in our phone book, we certainly won't be able to look up their registration number in the governmental database! So what we need is a function that will take the results from the first computation, and put it into the second lookup, but only if we didn't get
Nothing the first time around. If we did indeed get
Nothing from the first computation, or if we get
Nothing from the second computation, our final result should be
comb :: Maybe a -> (a -> Maybe b) -> Maybe b comb Nothing _ = Nothing comb (Just x) f = f x
As you might have guessed by now,
comb is just
(>>=); and so we can chain our computations together:
getRegistrationNumber :: String -- their name -> Maybe String -- their registration number getRegistrationNumber name = lookup name phonebook >>= (\number -> lookup number governmentalDatabase)
If we then wanted to use the result from the governmental database lookup in a third lookup (say we want to look up their registration number to see if they owe any car tax), then we could extend our
getTaxOwed :: String -- their name -> Maybe Double -- the amount of tax they owe getTaxOwed name = lookup name phonebook >>= (\number -> lookup number governmentalDatabase) >>= (\registration -> lookup registration taxDatabase)
Or, using the
getTaxOwed name = do number <- lookup name phonebook registration <- lookup number governmentalDatabase lookup registration taxDatabase
Let's just pause here and think about what would happen if we got a
Nothing anywhere. Trying to use
>>= to combine a
Nothing from one computation with another function will result in the
Nothing being carried on and the second function ignored (refer to our definition of comb above if you're not sure). That is, a
Nothing at any stage in the large computation will result in a
Nothing overall, regardless of the other functions! Thus we say that the structure of the
Maybe monad propagates failures.
The key features of the
Maybe monad are that:
- It represents computations that could fail.
- It propagates failure.
Another trait of the
Maybe monad is that it is "open": if we have a
Just value, we can extract its associated value by pattern matching (which is what we did in the first implementation of
safeLogSqrt). That is is not true for all monads; often, they will be designed so as to help you by hiding unnecessary details. It is perfectly possible to make a "no-exit" monad, from which it is never possible to extract "pure" values, the obvious example being the
IO monad: in this way it is impossible not to notice that an operation involves input/output (and thereby side effects), because any I/O operation will carry around the
IO monad, which functions as a warning sign.
- The definitions in the actual instance in
Data.Maybeare written a little differently, but are fully equivalent to these.
- Check the chapter about maps in Haskell in Practice for a different, and potentially more useful, implementation.