Perplexed by Monads? Great! I’m going to show you if you’re comfortable with classes and objects, you’re already acquainted with Monads.

Before we start, what is a Monad? Well according to Wikipedia:

Monads are a way to structure computations as a sequence of steps, where each step not only produces a value but also some extra information about the computation, such as a potential failure, non-determinism, or side effect.

You can think of Monads as a design pattern for simulating side-effects. This is super helpful for a lazy and pure programming language where side-effects are prohibited (purity) and execution order isn’t guaranteed (laziness).

If none of that makes sense, don’t worry. Just keep these 3 things in mind:

  1. sequence of steps
  2. produces a value
  3. extra information about the computation

For illustration I’ll be using Python and Haskell. If you’re unfamiliar with Haskell, checkout my whistle-stop tour to get up to speed.

Python on the other hand is basically runnable pseudocode, but to keep us all on the same page I’ll explain things as we go.

A Python I/O Manager#

In object-orientated programming, we create classes and objects to represent components of the problem we’re solving. Let’s create an I/O manager class in Python:

 1import sys
 2
 3class IOManager:
 4
 5    def __init__(self):
 6        self.stdout = sys.stdout
 7        self.stdin = sys.stdin
 8        self.stderr = sys.stderr
 9
10    def read_line(self) -> str:
11        return self.stdin.readline()
12
13    def write_line(self, msg: str) -> None:
14        self.stdout.write(msg + "\n")
15
16    def write_error(self, msg: str) -> None:
17        self.stderr.write("Error: " + msg + "\n")

To summarize, we:

  1. Initialise the IOManager class with 3 I/O streams: stdin, stdout and stderr.
  2. Define 3 methods:
    • read_line: reads a line from stdin.
    • write_line: takes a str argument and writes to the terminal with a newline at the end.
    • write_error: same as write_line except it writes to stderr.

Pretty straightforward right?

Let’s use our IOManager to write a little program.

  • We’ll ask the user their name.
  • Check their name is valid
  • Print a message saying “Hello <name>!” if it is, and “<name> isn’t a valid name!” if not.

 1import sys
 2
 3class IOManager:
 4
 5    def __init__(self):
 6        self.stdout = sys.stdout
 7        self.stdin = sys.stdin
 8        self.stderr = sys.stderr
 9
10    def read_line(self) -> str:
11        return self.stdin.readline()
12
13    def write_line(self, msg: str) -> None:
14        self.stdout.write(msg + "\n")
15
16    def write_error(self, msg: str) -> None:
17        self.stderr.write("Error: " + msg + "\n")
18
19# create a new IOManager instance
20my_io_manager: IOManager = IOManager()
21
22my_io_manager.write_line("What is your name?")
23
24name_str: String = my_io_manager.read_line()
25
26# check each character is alphabetic
27# anything else isn't valid!
28if not all(c.is_alpha() for c in name_str):
29    my_io_manager.write_error(name_str + " isn't a valid name!")
30else:
31    my_io_manager.write_line("Hello " + name_str + "!")

Running the program:

What is your name?
> Dave
Hello Dave!

And with bad input:

What is your name?
> Dave123
Error: Dave123 isn't a valid name!

🥱 Kind of boring… But if you can follow this, you’re halfway there with Monads!

Now that we’ve implemented our IOManager, let’s implement the same program in Haskell with Monads.

I…Oh?#

To get started, let’s think about printing to the screen.

putStrLn is a Haskell function which takes a String argument and prints it to the screen. It has the following type signature:

putStrLn :: String -> IO ()

Hmm.. What on Earth is this IO ()? Well, let’s look again at the write_line function from our IOManager class:

def write_line(self, msg: str) -> None:
    self.stdout.write(msg + "\n")

Interesting. Our write_line method takes a str and “returns” None, meaning it doesn’t return anything. putStrLn also takes a String, but it returns this strange IO () thing. What gives?

Well think of it this way: our IOManager object handles reading and writing to the terminal. Haskell does the same through the IO Monad. The IO Monad is just like IOManager: it interacts with the outside world for us, and returns nice, pure data back.

When we call write_line or read_line from the IOManager, we don’t care how it works, we just want the “effect” to happen (printing to the terminal or getting some input from the user). The same is true for Monads - we don’t care how the Monad does it’s thing, we just want the thing to happen.

With that in mind, IO () means “do some I/O and give us () back”1, or in other words “do some I/O and return nothing”. Kind of makes sense right? When we print something to the screen, we only want to print to the screen - we don’t want anything back.

Let’s look at the type of the Haskell getLine function, which reads a line from the terminal, and compare it with read_line from the IOManager:

getLine :: IO String
def read_line(self) -> str:
    return self.stdin.readline()

The IOManager read_line function takes no arguments and returns a str. The Haskell getLine function takes no arguments and returns IO String.

Interesting… These type signatures are almost the same, except getLine wraps it’s result (the String) within IO. That’s down to more Monad awesomeness: Monads provide a context, meaning you can represent different situations as Monadic data types.

For example, computations which may fail can be represented with a Monad called Maybe. The Maybe Monad wraps successful results in Just, and returns a Nothing when a failure occurs.

maybeDivide :: Int -> Int -> Maybe Int
maybeDivide a b = if b == 0 then Nothing
                  else Just (a `div` b)
  • return Nothing if you try to divide something by 0.
  • otherwise, return Just the result of dividing a by b.

Notice the result is wrapped in the Maybe context (the Maybe Int).

Going back to the str returned by read_line, we don’t have to wrap it in anything, because read_line is defined within the context of IOManager - it (and the other methods) are defined inside the IOManager class, so that is their context.

With Monads the context is explicit (the Maybe in ‘Maybe Int’ or the IO in ‘IO String’) whereas with our IOManager it’s implicit - it’s still there but it’s hidden from us.

Ok we’ve got a mental model for Monads now: it’s like an object which handles some impure action for us and wraps results in a context. Great - we’ve got a way of doing an impure action in a pure language.

But most imperative programs aren’t a single step, they’re a sequence. They “do A then do B then do C” and so on. Also they likely keep data from previous steps to use in later steps. Our program for checking a user’s name certainly does:

 1import sys
 2
 3class IOManager:
 4
 5    def __init__(self):
 6        self.stdout = sys.stdout
 7        self.stdin = sys.stdin
 8        self.stderr = sys.stderr
 9
10    def read_line(self) -> str:
11        return self.stdin.readline()
12
13    def write_line(self, msg: str) -> None:
14        self.stdout.write(msg + "\n")
15
16    def write_error(self, msg: str) -> None:
17        self.stderr.write("Error: " + msg + "\n")
18
19# create a new IOManager instance
20my_io_manager: IOManager = IOManager()
21
22my_io_manager.write_line("What is your name?")
23
24name_str: str = my_io_manager.read_line()
25
26# check each character is alphabetic
27# anything else isn't valid!
28if not all(c.is_alpha() for c in name_str):
29    my_io_manager.write_error(name_str + " isn't a valid name!")
30else:
31    my_io_manager.write_line("Hello " + name_str + "!")

See how name_str is used after we get it from write_line on line 24.

How can we sequence our impure steps in Haskell just like we sequence them in an imperative language?

High-Ho, High-Ho, Into IO We Go!#

To build up to sequencing, let’s implement our “check user’s name” program in Haskell.

Starting at the top, we’ll ask the user their name:

whatIsYourName :: IO ()
whatIsYourName = putStrLn "What is your name?"
  • putStrLn prints a string to the terminal and has type String -> IO ().

Ok now we’ve asked for their name, let’s actually get it from the terminal:

getName :: IO String
getName = getLine
  • getLine reads a line from the terminal and has type IO String.

Cool, now lets implement a function which checks that the user’s name is valid (only contains alphabetic characters), and prints “Hello <name>!” if it is, and “<name> isn’t a valid name!” if it isn’t.

To check a character is alphabetic, we’ll use the function isAlpha from the Data.Char module. isAlpha has type Char -> Bool.

This will work for checking a single character, but we want to check all the characters in the person’s name. We can use another function called all which has type Foldable t => (a -> Bool) -> t a -> Bool.

We won’t go into too much detail, as all we need to know is that all takes a predicate (the (a -> Bool)), some kind of structure to check against (the t a), and returns True if each element in the structure returns True for the predicate. For us, the predicate is isAlpha and our structure is our String.

Cool, let’s implement that.

We’ll call our function printResult, and it will take a single String argument which is the input we’ve just read from the temrinal:

import Data.Char(isAlpha)

printResult :: String -> IO ()
printResult name = if nameIsValid then putStrLn ("Hello " ++ name ++ "!")
                   else putStrLn (name ++ " isn't a valid name!")
                   where nameIsValid = all isAlpha name

Nice, we’ve got all the logic down:

  • We ask user their name with whatIsYourName.
  • We get their name from the terminal with getName.
  • We check their name is valid, and print the result using printResult.

Now we need to tie it all together.

Right off the bat there’s an issue - we can’t really represent sequencing easily since our functions are kind of a one-shot-deal. Functions in Haskell can be defined recursively, so maybe we can define our sequencing recursively.

Let’s define a function called step, which will take an Int argument and will do a specific part of our program depending on the argument. For example, step 1 shoud run whatIsYourName since that’s the first step of our program. step 2 should ask the user their name, then we’ll do our check and printResult.

step :: Int -> IO ()
-- ask their name then go to step 2, wait... what is '>>'?
step 1 = whatIsYourName >> step 2
-- show the person's name, then finish
step 2 = getName >>= printResult
-- there nothing after step 2!
step _ = return ()

Here is the complete code somewhat tidied up:

 1module main where
 2
 3import Data.Char(isAlpha)
 4
 5-- ask their name
 6whatIsYourName :: IO ()
 7whatIsYourName = putStrLn "What is your name?"
 8
 9getName :: IO String
10getName = getLine
11
12-- check the person's name is valid
13printResult :: String -> IO ()
14printResult name = if nameIsValid then putStrLn ("Hello " ++ name ++ "!")
15                   else putStrLn (name ++ " isn't a valid name!")
16                   where nameIsValid = all isAlpha name
17
18step :: Int -> IO ()
19-- ask their name then go to step 2
20step 1 = whatIsYourName >> step 2
21-- show the person's name, then finish
22step 2 = getName >>= printResult
23-- there nothing after step 2!
24step _ = return ()
25
26--  start at step 1
27main :: IO ()
28main = step 1

YUCK! That’s about as readable as your doctor’s handwriting! Surely there’s a better way (spoiler: there is).

Woah-oh! Sweet Child O’>>=#

Let’s zoom in on this funny looking >>= symbol. Can you spot it above? This is bind.

bind sequences “impure” (Monadic) actions together. It can take something like IO String, unwrap the IO, then pass the String to the function on it’s right (“bind” it). This is what getLine >>= printResult does: it takes the IO String returned from getLine, removes the IO wrapper, and passes the String to printResult which, as you can see, expects a String argument (called name) and does some more IO ()2.

As cool as >>= is, using it can be unwieldy, so the Haskell team invented ‘do’ notation. If we use the do notation, and leverage the power of bind more effectively, we can rewrite our function in a more sane way:

 1module main where
 2
 3import Data.Char(isAlpha)
 4
 5main = do putStrLn "What is your name?"
 6          name <- getLine  -- read line from terminal
 7          if nameIsValid then
 8            putStrLn ("Hello " ++ name ++ "!")
 9          else putStrLn ("Exception " ++ name ++ " is not a valid name!")
10          where nameIsValid = all isAlpha name

Ahh! Much better! We’ve substituted dear old >>= for do and <- . <- serves the same function as >>=: take an IO Something on the right, unwrap the IO, and assign the Something to the variable (name in our case) on the left. And with that, we’re done!

Conclusion#

  • Side effects are all about changing state in a specific order.
    • We don’t launch the missiles before checking the user has clicked ‘Launch’.
  • A Monad is a data type which represents an impure action, such as I/O.
  • Monad’s provide a “computational context” - think of the similar purpose of the IOManager and the IO Monad.
  • Monad’s can wrap “pure” values such as String, Int, or ().
  • These results are the result of doing the action.
  • We define order using >>= (bind) if we want to keep stuff from previous steps.
  • We can use the do notation when sequencing Monads together, to make life easier.

We’ll leave it there for now but there is so much more to this story! We’ll talk more about Monads and other brain-twisting concepts, but on our journeys of discovery we’ll see that, with a little thought and intuition comes immense power!

Bye for now!


  1. We call () Unit and it’s equivalent to None in Python or void in C-like languages: it’s the type of data with no values. ↩︎

  2. Also in our recursive example above is >>, called ‘then’. It’s like ‘>>=’ except it doesn’t unwrap or bind anything - think of it as “do the thing on the left, then the right”. ↩︎