When you read about which programming languages you can learn to understand some language concepts, Haskell is often stated as represantant for a pure functional language. Often the next thing you can read is that Haskell has the unique concept of Monads, usually with no further explanation. I’ll try to explain in a simple way what Monads are about and how they are used in Haskell to keep the language pure.

A simplified explanation of Monads

The use case for Monads is that you have a sequence of operations and you want to keep some context that should not be lost between the operations. A few examples what a context can be and their corresponding Monads: the information that a previous step was not successful (Maybe or Either Monads), handling multiple possibilities (List Monad), tagging an operation that has side-effects (IO Monad) or logging information about previous steps (Writer Monad).

The Monad type class

As very first step, we’ll look at how Monads are definied in Haskell. Actually they are a type class (in other languages you’d call it interface) with following definition

class Monad m where
    return :: a -> m a
    (>>=) :: m a -> (a -> m b) -> m b
    (>>) :: m a -> m b -> m b
    x >> y = x >>= \_ -> y
    fail :: String -> m a
    fail msg = error msg

So in the type class we have to functions that need to be implemented for every Monad (return and >>=) and two functions which have a default implementation (>> and fail).

What is the purpose of these functions?

  • The return function is used to wrap a normal value into the Monad.
  • The >>= takes a Monad value and a function that returns a Monad value from the wrapped value
  • The >> allows to chain two Monadic values. In its default implemenation, it just ignores the first parameter and keeps the second
  • The fail function wraps an error message into a Monad. In its default implementation it ends the program with an error.

The Maybe Monad as example

One of the most simple Monads is the Maybe Monad. Maybe a is a class like Optional<T> in Java that can either have a value (e.g. if a=Int: Just 10) or no value (Nothing). The purpose of this Monad is to chain some calculations and keep track if any of these calculations was Nothing. In that case everything which follows will be Nothing as well.

That means that the functions are defined like this:

return x = Just x

Just x >>= f = f x
Nothing >>= _ = Nothing

Let’s look at some example: We’ll define a function that keep tracks of the number of items are available in a store. Everytime something is bought, we substract the number. When no items are left, we have Nothing and it always stays Nothing. We define a function takeFromStore:

takeFromStore :: Int -> Int -> Maybe Int
takeFromStore x y 
    | x < 0 = error "Cannot remove value smaller 0"
    | y > 0 &&  x < y = Just $ y - x
    | otherwise = Nothing

But wait: we need Int -> Maybe Int as signature to apply it with >>=!

You’re right and we’ll use the fact that we have currying in Haskell, so that we can partially apply function e.g. (takeFromStore 10) and use them as parameter for >>=.

E.g. let’s consider the following sequence:

Just 200 >>= takeFromStore 10 >>= takeFromStore 50 >>= takeFromStore 23

This will return Just 17. If we continue and apply takeFromStore 18, we’ll get Nothing as result and from then on, we’ll always get Nothing.

Actually, the main purpose of the Maybe Monad is to keep the state to Nothing once it is in this stage. This is a good way to mark a sequence of operations as failed.

do blocks

As alternative notation to chains of >>= and >>, you can also use do blocks to perform Monad actions. A sequence of takeFromStore can also be written in a function like this:

takeFromStoreMultipleTimes :: Maybe Int
takeFromStoreMultipleTimes = do
    initial <- Just 500
    after1Step <- takeFromStore 234 initial
    after2Steps <- takeFromStore 122 after1Step
    after3Steps <- takeFromStore 300 after2Steps
    takeFromStore 5 after3Steps

This is equivalent to Just 500 >>= takeFromStore 234 >>= takeFromStore 122 >>= takeFromStore 300 >>= takeFromStore 5. The disadvantage is that you have to bind every intermediate result to a variable. It looks pretty much like code in an imperative language.

Now comes the thing that makes the do notation a bit counter-intuitive sometimes:

takeFromStoreButReturnOldValue :: Int -> Maybe Int
takeFromStoreButReturnOldValue initialCount = do
    initial <- Just initialCount
    after1Step <- takeFromStore 234 initial
    after2Steps <- takeFromStore 122 after1Step
    return after1Step

When you call takeFromStoreButReturnOldValue 400, you’ll get Just 166 as result, as you would expect. When you call takeFromStoreButReturnOldValue 300 you may expect to get Just 66, but instead it returns Nothing.

What has happened? There are two reasons why we do not get the expected result here. The first one is that return does not work like you know it from languages like Java or C. It does not return a value, but just wraps it into a Monad. The second unusual thing is that the result of the function is not return after1Step but Just initialCount >>= takeFromStore 234 >>= takeFromStore 122 >> return after1Step. So we apply the >> function with return after1Step as second parameter. As the first parameter is the result of the previous line (so takeFromStore 122 after1Step which equals Nothing) the result of the do block is

Nothing >> return after1Step
   = Nothing >> Just 66
   = Nothing >>= (\_ -> Just 66)
   = Nothing

For me this is the most crucial thing to understand about do blocks: they may look like some imperative way to write Haskell programs, but every line that doesn’t start with let has to be a Monad action and is either applied to >> or >>= functions, so that the context (in the case of Maybe: did we have a Nothing value somewhere) is passed from step to stop. So even if the result of a step happening before is returned, we don’t lose the “side effects” that happen in steps that happened later.

The Writer Monad

A very good example for a Monad is the Writer Monad with two type parameters, Writer a b where type a has to be a Monoid (a type that implements the operations mappend and mempty for example a list). For example we can define the following operation to log numbers:

import Control.Monad.Writer
logNumber :: Int -> Writer [String] Int
logNumber x = writer (x, ["Got number: " ++ show x])

We can now use it in the following code:

multWithLog :: Writer [String] Int
multWithLog = do
    a <- logNumber 3
    b <- logNumber 5
    logNumber 10
    return 500

This will return WriterT (Identity (500,["Got number: 3","Got number: 5","Got number: 10"])). Here as well you can see that even if we only have return 500 (which equals WriterT (Identity (500,[])) we still get the logs from the previous operations as the context is preserved. So the Writer Monad can be used to manage an event log for example.

The IO Monad

Finally we’ll finish with the IO Monad. The IO Monad can be seen as a context showing that we are performing unpure operations having side effects. You can think of the context being the sequence of side-effects that has occured. Here is an example of how to use the IO Monad:

import System.IO

main = do
    name <- getName
    putStrLn $ "Hello, " ++ name ++ "!"

getName :: IO String
getName = do
    putStr "What's your name? "
    hFlush stdout
    name <- getLine
    return name

We have two functions here that perform IO actions. The first one is main that will be executed when you run the compile program and the second one is getName that reads a name from standard input. The getName function could also have been written like this without do block: getName = putStr "What's your name? " >> hFlush stdout >> getLine

What is special about the IO Monad is that unlike most other Monads there is no function IO a -> a to extract the value from the IO Monad (expect the <- operator that can only be used inside a do block). So you cannat get rid of the IO Monad and therefor cannot use any IO actions inside your function that return something of type Int for example. This is Haskell’s way of separating unpure IO actions from pure side-effect free code.

Conclusion

Monads are not so tough to reason about once you stop thinking that they are complicated. They are just a wrapper that keeps a context attached to a value.