Monads

Welcome to part 3 of our Monads series! This time, we'll actually start working with monads themselves! By this point, you should already understand Functors from part 1 of the series, and Applicatives from part 2. So you should have some idea of how abstract structures work.

A monad is another type of abstract, functional structure. Let's explore what makes it different from our first two structure examples.

What is a Monad?

A monad is a computational context. It provides a structure that allows you to chain together operations that have some kind of shared state or similar effect. Whereas "pure" code can only operate on explicit input parameters and affect the program through explicit return values, operations in a monad can affect other computations in the chain implicitly through side effects, especially modification of an implicitly shared value.

How are monads represented in Haskell?

Like functors and applicatives, monads are represented with a typeclass in Haskell.

class (Applicative m) => Monad m where
  ...

Just as every applicative is a functor, every monad is also an applicative. These three structures form a dependency chain, with each level having fewer structures.

The minimal description of a monad class can be given a few ways, but the simplest is with the return function and the bind operator, which looks like (>>=).

class (Applicative m) => Monad m where
  return :: a -> m a
  (>>=) :: m a -> (a -> m b) -> m b

At first glance, this looks similar to the definition for the Applicative. The return function has essentially the same type signature as the pure function. (And indeed, these almost always have the same implementation). So a monad must also have a "default" way to wrap a value in the structure.

The bind operator is similar to the application operator in that it chains two operations, with one of them being function related.

Let's compare the three primary functions of our typeclasses: fmap, the "apply" operator, and the "bind" operator. We'll use a flipped version of the bind operator ((=<<)) data-preserve-html-node="true" to better show the pattern.

(<$>) :: (a -> b) -> f a -> f b
(<*>) :: f (a -> b) -> f a -> f b
(=<<) :: (a -> f b) -> f a -> f b

All these operators take a function of some kind, and then a structure wrapped over an a type, and then produce a structure wrapping a b type. They just vary in what the function looks like. For the functor, the function is a normal pure function. For applicatives, the function is still pure, but wrapped in the structure. Now with monads, our function argument takes a "pure" input but produces an output in the structure.

Basic Monad Example

Just as Maybe is a functor and an applicative functor, it is also a monad! We can start with how Maybe implements the Monad typeclass.

instance Monad Maybe where
  return = Just
  Nothing >>= _ = Nothing
  Just a >>= f = f a

In principle, this is similar to the Applicative definition. We use Just for return just like we used it for pure. When we try applying a function over Nothing, we just get Nothing. Otherwise, we apply the function over the value.

But let's bring in our definition of a monad. What does it mean to describe Maybe as a computational context?

The Maybe monad encapsulates the context of failure. Essentially, the Maybe monad lets us abort a series of operations whenever one of them fails. This allows future operations to assume that all previous operations have succeeded. Here's some code to motivate this idea:

maybeFunc1 :: String -> Maybe Int
maybeFunc1 "" = Nothing
maybeFunc1 str = Just $ length str

maybeFunc2 :: Int -> Maybe Float
maybeFunc2 i = if i `mod` 2 == 0
  then Nothing
  else Just ((fromIntegral i) * 3.14159)

maybeFunc3 :: Float -> Maybe [Int]
maybeFunc3 f = if f > 15.0
  then Nothing
  else Just [floor f, ceiling f]

runMaybeFuncs :: String -> Maybe [Int]
runMaybeFuncs input = case maybeFunc1 input of
  Nothing -> Nothing
  Just i -> case maybeFunc2 i of
    Nothing -> Nothing
    Just f -> maybeFunc3 f

We have three different functions that could fail. We combine them all in runMaybeFuncs. But at each stage, it has to check if the previous result succeeded. It would be very tedious to continue this pattern.

The Maybe monad helps us fix this. Here's what this function looks like using the bind operator.

runMaybeFuncsBind :: String -> Maybe [Int]
runMaybeFuncsBind input = maybeFunc1 input >>= maybeFunc2 >>= maybeFunc3

It's much cleaner now! We take our first result and pass it into the second and third functions using the bind function. The monad instance handles all the failure cases so we don't have to!

Let's see why the types work out. The result of maybeFunc1 input is simply Maybe Int. Then the bind operator allows us to take this Maybe Int value and combine it with maybeFunc2, whose type is Int -> Maybe Float. The bind operator resolves these to a Maybe Float. Then we pass this similarly through the bind operator to maybeFunc3, resulting in our final type, Maybe [Int].

Your functions will not always combine so cleanly though. This is where do notation comes into play. This notation allows us to write monadic operations one after another, line-by-line. It almost makes our code look like imperative programming. We can rewrite the above as:

runMaybeFuncsDo :: String -> Maybe [Int]
runMaybeFuncsDo input = do
  i <- maybeFunc1 input
  f <- maybeFunc2 i
  maybeFunc3 f

The <- operator is special. It effectively unwraps the value on the right-hand side from the monad. This means the value i has type Int, even though the result of maybeFunc1 is Maybe Int. The bind operation happens under the hood. If the function returns Nothing, then the entire runMaybeFuncs function will return Nothing. Observe that we do not unwrap the final line of the computation. Our function result is Maybe [Int], so our final line is Maybe [Int].

At first glance, this looks more complicated than the bind example. However, it gives us a lot more flexibility. It is particularly helpful when one monadic function depends on multiple previous functions.

The IO Monad

The IO Monad is perhaps the most important monad in Haskell. It is also one of the hardest monads to understand starting out. Its actual implementation is too intricate to discuss when first learning monads. So we'll learn by example.

What is the computational context that describes the IO monad? IO operations can read information from or write information to the terminal, file system, operating system, and/or network. They interact with systems outside of your program. If you want to get user input, print a message to the user, read information from a file, or make a network call, you'll need to do so within the IO Monad.

The state of the world "outside your program" can change at virtually any moment, and so this context is particularly special. So these are "side effects". We cannot perform them from "pure" Haskell code.

Now, the most important job of pretty much any computer program is precisely to perform this interaction with the outside world. For this reason, the root of all executable Haskell code is a function called main, with the type IO (). So every program starts in the IO monad.

From here you can get any input you need, call into "pure" code with the inputs, and then output the result in some way. The reverse does not work. You cannot call into IO code from pure code like you can call into a Maybe function from pure code.

Let's look at a simple program showing a few of the basic IO functions. We'll use do-notation so you can see how it is similar to the Maybe monad. We list the types of each IO function for clarity.

main :: IO ()
main = do
  -- getLine :: IO String
  input <- getLine
  -- Assign an expression name. Can be done in ANY monad.
  let uppercased = map Data.Char.toUpper input
  -- print :: String -> IO ()
  print uppercased

So we see once again each line of our program has type IO a. (Note: a let statement can occur in any monad). Just as we could unwrap i in the maybe example to get an Int instead of a Maybe Int, we can use <- to unwrap the result of getLine as a String. We can then manipulate this value using string functions, and pass the result to the print function.

This is a simple echo program. It reads a line from the terminal, and then prints the line back out capitalized to the terminal. Hopefully it gives you a basic understanding of how IO works. We'll get into more details in the next couple parts of the series.

What separates Monads from Applicatives?

The key word that separates these is context. We cannot really determine the structure of "future" operations without knowing the results of "past" operations, because the past can alter the context in which the future operations work. With applicatives, we can't get the final function result without evaluating everything, but we can determine the structure of how the operation will take place. This allows some degree of parallelism with applicatives that is not generally possible with monads.

What are the Monad Laws?

Monads have three laws. The first two are the "left" and "right" identity laws. The third is the associativity law.

-- Left Identity
m >>= return m

-- Right Identity
return x >>= f = f x

-- Associativity
(m >>= f) >>= g = m >>= (\x -> f x >>= g)

These are similar in principle to the laws we've seen for functors and applicatives already. Identity laws for monads specify that return by itself shouldn't really change anything about the structure or its values. The associativity law is difficult to parse like some of the applicative laws, but it has a similar meaning. If we change the grouping of operations, we should still get the same result.

Unlike applicatives, we can't resolve the structure of later operations without the results of earlier operations quite as well because of the extra context monads provide. But we can still group their later operations into composite functions taking their inputs from earlier on, and the result should be the same.

While these laws are difficult to understand just by looking at them, the good news is that most of the instances you'll make of these classes will naturally follow the laws, so you don't have to worry about them too much.

Conclusion

Hopefully this article has given you a base level understanding of what a monad is. But perhaps some more examples of what example a "computational context" means would be useful to you. The Reader, Writer, and State monads each provide a concrete and easily understood context that can be compared easily to function parameters. So you can learn more about those in Part 4 (Reader/Writer monads) and Part 5 (State monad).

And if you want some in-depth practice with the basics of monads, you should take a look at our Making Sense of Monads course! It covers all the material in this series with deeper video lectures, and offers you the chance to practice your skills with programming exercises and two practice projects!