Making your brain tingle: Explaining Monads

My attempt at explaining monads

It is an unwritten law, that everybody learning Haskell sooner or later has to write a tutorial or explanation of the topic of monads. (The fourth monad law.)1

So here is my attempt at explaining monads, I will try to give an intuition on what monads are from the practical view of a developer. I’m going to use examples in Haskell, so yes, this time I’m going to assume, some basic knowledge of Haskell – but a seasoned developer might still be able to figure out the essence of the examples without learning Haskell.

Monads are functors…

First and foremost, monads are functors. And so the metaphor of containers works for them as well, at least as a first approximation. But what makes a functor a monad?

To make this intuitive lets start with an example of a simple task: Lets say we have a list of integers, and we want to extend the list by inserting the result of the multiplication with two, after each of them. So given a list [1,2,3] we want to get the list [1,2,2,4,3,6].

As written in my last article for each functor there is a way to map functions, so that, staying in the picture of containers, they operate on the content of the container. In Haskell the function fmap does this mapping. Lists are functors, so lets try to solve the task using fmap:

addTimesTwo :: Int -> [Int]
addTimesTwo x = [x, x*2]

-- and fmap it into our functor:
fmap addTimesTwo [1..3]
-- returns: [[1,2],[2,4],[3,6]]

Ooops, that’s not what we wanted to do. I could now try this or that, but – spoiler ahead – there is absolutely no way this could be done using just fmap. The reason is that an fmap-ed function by definition can only operate on the content of our containers, but our task includes the requirement to enhance the list, which would mean to change the container itself.

In terms of types: having a list [Int] and a function Int -> [Int], when mapping the function to the list we will get a result of the type [[Int]], but we wanted a [Int]. Or more general: having a functor F a and a function a -> F a mapping will give us F (F a).

Of course, the task at hand can still be solved relatively easily, we just have to operate on the list (our functor) directly:

addTimesTwoInList :: [Int] -> [Int]
addTimesTwoInList [] = []
addTimesTwoInList (x:xs) = x : x*2 : addTimesTwoInList xs

addTimesTwoInList [1..3]
-- returns: [1,2,2,4,3,6]

That wasn’t to hard, was it? But we lost a lot of abstraction. Let’s say we need another function, doing the same thing, but with a different math operation, for example adding 42 instead of multiplication with two? We can write this function, too:

addPlusFortytwoInList :: [Int] -> [Int]
addPlusFortytwoInList [] = []
addPlusFortytwoInList (x:xs) = x : x+42 : addPlusFortytwoInList xs

addPlusFortytwoInList [1..3]
-- returns: [1,43,2,44,3,45]

That’s a lot of duplicated logic. Let’s see if we can factor out the list manipulation in a way, so that we can use our original function addTimesTwo:

applyToList :: (a -> [b]) -> [a] -> [b]
applyToList _ [] = []
applyToList f (x:xs) = f x ++ applyToList f xs

addTimesTwo :: Int -> [Int]
addTimesTwo x = [x, x*2]

addPlusFortytwo :: Int -> [Int]
addPlusFortytwo x = [x, x+42]

-- Try it:
applyToList addTimesTwo [1..3]
-- returns: [1,2,2,4,3,6]

applyToList addPlusFortytwo [1..3]
--returns: [1,43,2,44,3,45]

That’s much better! So what have we done here? Well, we wrote a special function for function application to a list.

…with specific rules for joining in function application

While the type of standard function application is: (a -> b) -> a -> b we created a new variation with the type (a -> [b]) -> [a] -> [b], that’s quite the similarity! The difference is the asymmetry in the argument type of the function we apply and the type of argument we provide: we only give the contents of our container (the list) to the function, which returns a new container of the same functor type (list)2, then it is the task of our new function application method, to join the two containers: the one of the argument (which it kind of kept back from the called function) and the one of the result.

As we invented a way to apply functions, we can now of define function combination as well:

after :: (b -> [c]) -> (a -> [b]) -> (a -> [c])
after g f = applyToList g . f

-- and test it:
applyToList addPlusFortytwo (applyToList addTimesTwo [1..3])
-- returns: [1,43,2,44,2,44,4,46,3,45,6,48]
applyToList (addPlusFortytwo `after` addTimesTwo) [1..3]
-- returns: [1,43,2,44,2,44,4,46,3,45,6,48]

Ok, so that’s nice, now we have function combination for functors. But we have only defined our clever new function application for lists. The concept of automatically joining containers during function application is generic, so lets implement it for another functor. The canonical second example is Maybe:

applyToMaybe :: (a -> Maybe b) -> Maybe a -> Maybe b
applyToMaybe _ Nothing = Nothing
applyToMaybe f (Just x) = f x

after2 :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c)
after2 g f = applyToMaybe g . f
-- and some functions, to test it:
import Data.Char (toLower)
import Data.List (elemIndex)

firstChar :: String -> Maybe Char
firstChar "" = Nothing
firstChar (c:_) = Just c

positionInAlphabet :: Char -> Maybe Int
positionInAlphabet c = (+1) <$> elemIndex (toLower c)
                       "abcdefghijklmnopqrstuvwxyz"

-- put it to the test:
applyToMaybe (positionInAlphabet `after2` firstChar) (Just "")
-- returns: Nothing
applyToMaybe (positionInAlphabet `after2` firstChar) (Just "Foo")
-- returns: Just 6
applyToMaybe (positionInAlphabet `after2` firstChar) (Just ":-)")
-- returns: Nothing

Ok, function application and combination with automatic handling of the functor joining works in this case, too. The only ugliness left is that we are inventing a new name for the application function (and therefore for the combination function) per functor. Unfortunately we can not write one generic apply for all functors, as they are to diverse in their semantics (just as we can not write one common version of fmap for all functors). So, we will need a specific implementation per functor, but we can make it ad-hoc polymorphic through a type class:

class Functor f => FancyFunctor f where
  apply :: (a -> f b) -> f a -> f b
  after :: (b -> f c) -> (a -> f b) -> (a -> f c)
  after g f = apply g . f

instance FancyFunctor [] where
  apply _ [] = []
  apply f (x:xs) = f x ++ apply f xs

instance FancyFunctor Maybe where
  apply _ Nothing = Nothing
  apply f (Just x) = f x

Now we can write function combination for our different fancy functors in a unified way:

apply (addPlusFortytwo `after` addTimesTwo) [1..3]
-- returns: [1,43,2,44,2,44,4,46,3,45,6,48]
apply (positionInAlphabet `after` firstChar) (Just "abc")
-- returns: Just 1

Sweet. But what has all that to do with monads? Well, we just invented them! Our special apply function is called bind in terms of monads in Haskell, and the after operator derived from it is called Kleisli composition. Functors for which these operations are defined are called monads, as is the matching type class in Haskell.

And yes, lists and Maybe are monads in Haskell, so the functions we just created are already predefined for them.

Some unnecessary confusion

So what are these functions called in standard haskell monads? Well, just like the standard function application $ and function combination . they are defined as infix operators in haskell: the commonly used bind operator is >>= and the matching Kleisli composition is written as >=>. But a quick glance at the types of the monad functions shows that the arguments are flipped compared to our versions:

(>>=) :: Monad m => m a -> (a -> m b) -> m b
(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c

So we have to write:

import Control.Monad

(Just "Example") >>= (firstChar >=> positionInAlphabet)
-- returns: Just 5
(Just ")-:") >>= (firstChar >=> positionInAlphabet)
-- returns: Nothing

I think, this is a pity as it obfuscates the similarity between the function application operator $ and the monadic bind. But that is only with the commonly used bind operator. There are versions of both operators with flipped arguments:

(positionInAlphabet <=< firstChar) =<< (Just "Hurray")
-- returns: Just 8
(addPlusFortytwo <=< addTimesTwo) =<< [1..3]
-- returns: [1,43,2,44,2,44,4,46,3,45,6,48]

These, in my opinion more consistent versions, are not commonly used, but at least they exist. It is worth mentioning though, that >>= is in prelude, while =<< is only in Control.Monad.

Give me my monad

There is one more function which must be defined for every monad and which I didn’t mention yet. It is return and its type is Monad m => a -> m a. So for any value, return gives you the value mapped to a monadic functor. So this is basically a generic version of a type constructor for monads. There are at least two reasons why this is needed:3

  1. It allows us to write polymorphic functions which can operate on any monad.

  2. It allows the creator of a monad, to do fancy stuff on the creation of a monadic value.

An example of state

Now that we have a first impression what monads are, and why they are useful. But what’s all the fuss about them being so powerful? Monads are able to solve all the problems with pure functional programming, like side effects and state, how is this possible?

The answer is, that functors can enhance, or embellish, values. Taking our picture of containers, think about putting extra notes on the container, or even equip the container with some elaborated machinery, which does fancy thing, each time the content of the container is changed…

I’ll end with an relatively simple example of such a monad: The task will be to log changes on a value. So we will have functions that not only operate on the value but in addition create messages. These messages shall be aggregated, so that, together with the final result after an arbitrary number of operations, we will also have a complete log of all operations performed.4

So lets define our new monad Logger:

module Logger (Logger(..), showLog)
where

import Control.Applicative (Applicative(..))
import Control.Monad       (liftM, ap)

data Logger a = Logger {value :: a, logs :: [String]}
  deriving Show

instance Functor Logger where
    fmap = liftM

instance Applicative Logger where
    pure x = Logger x []
    (<*>) = ap

instance Monad Logger where
    return = pure -- redundant since GHC 7.10 due to default impl
    (Logger x ls) >>= f = let (Logger r l) = f x in
                            Logger r $ ls ++ l

showLog :: Logger a -> String
showLog = unlines . logs

There are a few things to say about the code, basically all the magic happens in the definition of >>=: The logging text, which is already part of the container of the argument is merged with the logging text, which the function f provided with its result.

What might be confusing are the other instance definitions:

  • Functor: as monads are functors it comes as no surprise that we must implement fmap. The good thing: fmap can be defined in terms of the monadic functions,5 which is already done by Control.Monad, where it is called liftM (because it “lifts” the function into the monad), so we can simply use that.

  • Applicative: I have not written about applicative functors yet, and for now we can basically ignore them, but we have to define the instance as just like functor it’s a super class of monad. There are two mandatory functions which we must supply:

    • <*>, which again we can define it in terms of a function from Control.Monad

    • and pure which happens to be identical to return. Because Haskell’s default definition of return in the Monad class is pure it makes sense to define pure instead of defining return directly, it’s the same thing anyway.

The function showLog is just a convenience function I added for a nice output of the logging information.

Now lets write a small program, using our new monad:

import Logger

type Account = Logger Int

newAccount :: Int -> Account
newAccount n = Logger n ["Initial balance: " ++ show n ++ " Credits"]

modifyAccount :: Int -> Int -> Account
modifyAccount n old = Logger (old + n) msg
  where
    msg
      | n > 0 = ["Deposited " ++ show n ++ " Credits"]
      | n < 0 = ["Withdrew " ++ show (negate n) ++ " Credits"]
      | otherwise = ["Null operation"]

-- Use it:
main :: IO ()
main = do
  let account = newAccount 0
                >>= modifyAccount 42
                >>= modifyAccount (-23)
                >>= modifyAccount 256
                >>= modifyAccount 25
  putStrLn $ "Current balance: " ++ (show . value $ account)
  putStrLn "Account's History:"
  putStrLn $ showLog account

When run, it produces the output:

Current balance: 300
Account's History:
Initial balance: 0 Credits
Deposited 42 Credits
Withdrew 23 Credits
Deposited 256 Credits
Deposited 25 Credits

So what we achieved here, is a behavior as if the log was some kind of global variable (or global state) each call of modifyAccount appended its logging information to.

The main function of the example shows something else, namely why the default bind operator >>= has its merits: consecutive functions combined by bind can be understood as a series of “actions”, and in this sense its easier to read code, where the order of execution and the order of actions (functions) in the code are identical.

Learn more

I have left out a lot of (partly important) details (and interesting theory) in this article. Now that I hopefully have shown to you, that monads in practice are nothing extraordinarily hard to understand, I’d highly recommend to study the work by others, to get a more complete picture of the topic:

  • as with any topic related to category theory my first recommendation will be Category Theory for Programmers by Bartosz Milewski. You can read it on his Blog or watch it as ongoing series of talks, given by him self on YouTube.

  • being an essential part of Haskell, there is a lot of material in the Haskell Wiki on monads

  • The Department of Computer and Information Science (CIS) of the University of Pennsylvania has a nice summary on monads and especially the IO monad in their Haskell course

Comment

As always, I’d like to hear from you. Especially I would be interested if this article helped you with understanding monads in Haskell or just increased the confusion…

Kudos

I want to thank Bernhard Herzog for corrections and suggestions.


  1. The other three monad laws are part of the formal definition of a monad, you can learn about them e.g. in the Haskell Wiki

    [return]
  2. Note, only the type of the “container” (the functor) needs to match, not the type of the “content”.

    [return]
  3. There are much more profound reasons for return, but like all the other deeper theoretical stuff, smarter people than I am already have explained them much better than I ever could. See the Learn more section of this article.

    [return]
  4. Basically I nicked the idea from Bartosz Milewski’s lecture on Kleisli categories, which is no coincidence, as Kleisli categories are basically monads.

    [return]
  5. e.g. fmap f = (=<<) (return . f) [return]