From Haskell in Depth by Vitaly Bragilevsky In this article we’ll discuss a few ways that you can use monads to simplify code. |
Take 40% off Haskell in Depth by entering fccbragilevsky into the discount code box at checkout at manning.com.
Haskell brings a concept of a monad to a level of practical use. In fact, we tend to think about any monad in terms of functionality it provides. Monads simplify our code significantly, they give the power of abstraction, and they guarantee the understanding of the code. In this article we’ll talk about using monads in practice. They help us implement difficult algorithms clearly and correctly, and they allow short, concise code and maintain readability and ease of support.
A teaser: Maybe monad as a line saver
What do you think about a function returning Maybe a
for some type variable a
? Well, it’s a computation which can have no result at all. The next question: what if you’ve several such computations and your task is to produce overall result from them? In general, there are two basic strategies:
- if you need both results to produce final one and one of the computations gives you
Nothing
then you answer withNothing
; - if it’s enough for you to get one result of the two then you stick with one which isn’t Nothing.
The choice of the right strategy clearly depends on your goals but Haskell gives you abstractions for both: the first strategy is implemented by monadic bind >>=
for Maybe
monad and the second one—by monoid operation over Maybe
values.
Imagine that you’ve two associative lists: one with person name and phone number pairs, and another with phone number and corresponding location pairs. How do you find location by the persons’ name? Clearly this scenario demands for the first strategy, because we can’t proceed without getting phone number first and we’re forced to return Nothing
.
Let’s implement this scenario in code. We’ve the following types:
type Name = String type Phone = String type Location = String type PhoneNumbers = [(Name, Phone)] type Locations = [(Phone, Location)]
We also have function lookup
from Prelude
which is clearly a computation in Maybe
monad:
lookup :: Eq a => a -> [(a, b)] -> Maybe b
Now we can implement searching for location via >>=
for Maybe a
:
locByName :: PhoneNumbers -> Locations -> Name -> Maybe Location locByName pnumbers locs name = lookup name pnumbers >>= flip lookup locs
Although we’ve had to flip
arguments for lookup
to get a function suitable for monadic binding (flip lookup locs
subexpression has type Phone → Maybe Location
) the implementation looks quite concise and guarantees that we’ll get Just somelocation provided by two successful lookup calls.
An implementation without monadic bind looks more tedious:
locByName' :: PhoneNumbers -> Locations -> Name -> Maybe Location locByName' pnumbers locs name = case lookup name pnumbers of Just number -> lookup number locs Nothing -> Nothing
If you look at the implementation of >>=
for Maybe
you’ll see the code with pattern matching over Maybe a
value to the left of >>=
:
instance Monad Maybe where (Just x) >>= k = k x Nothing >>= _ = Nothing -- ...
We’ve saved a little of typing by reusing >>=
. This is all about it: we use monads for functionality and convenience they provide. The same idea works for Functor
and Applicative
too as we normally try to use the functionality they provide whenever it’s sufficient for our goals.
For another example, let’s suppose you’re given a String
representing number, something like "21"
. How do you double it? You must take special care here: what if String
is incorrect and doesn’t represent any number? Well, we can use readMay
from Safe
module:
readMay :: Read a => String -> Maybe a
You don’t want to multiply Nothing
by 2 or you’ll cause the Functor
to not work:
doubleStrNumber :: (Num a, Read a) => String -> Maybe a doubleStrNumber s = (*2) <$> readMay s
It works as expected:
ghci> doubleStrNumber "21" Just 42 ghci> doubleStrNumber "yy" Nothing
If you’ve two String
-represented numbers and need to compute their sum, then it’s a job for Applicative
:
plusStrNumbers :: (Num a, Read a) => String -> String -> Maybe a plusStrNumbers s1 s2 = (+) <$> readMay s1 <*> readMay s2
And it also works as a charm:
ghci> plusStrNumbers "3" "5" Just 8 ghci> plusStrNumbers "3" "x" Nothing
If you want to see more, check out the book on Manning’s liveBook platform here.