Haskell/Understanding monads/Maybe

From Wikibooks, open books for an open world
Jump to: navigation, search

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 return and (>>=) for Monad amounting to:[1]

    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.

Safe functions[edit]

The Maybe datatype provides a way to make a safety wrapper around functions which can fail to work for a range of arguments. head and 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 sqrt and 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 (safeDiv, safeLog, 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 Control.Monad module:

import Control.Monad((<=<))
safeLogSqrt' = safeLog <=< safeSqrt

Lookup tables[edit]

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[2]. 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 Nothing.

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 getRegistrationNumber function:

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 do-block style:

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.

Summary[edit]

The key features of the Maybe monad are that:

  1. It represents computations that could fail.
  2. 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.


Notes[edit]

  1. The definitions in the actual instance in Data.Maybe are written a little differently, but are fully equivalent to these.
  2. Check the chapter about maps in Haskell in Practice for a different, and potentially more useful, implementation.