Skip to content

Understand Monads

Monad is a fundamental concept in Haskell. Recently, I have reviewed them and made my mind clearer. Hence, I wrote down this blog. To understand them, we need to clearly understand some concepts, such as:

  • understand the do notation syntax sugar and learn about how de-sugar it to >>= format
  • understand the closures after de-sugaring
  • type constructor
  • understand how the >>= deals with the data held inside a monad
  • understand the runMonad function properly
  • clarify the difference between monad in category(math) and the monad in haskell

Do Syntax Sugar and De-sugar it to Bind

Because I already learned a lot about the monad, the basic concepts of return statement and >>= bind are omitted. In this topic, I try to focus on the do notation with its <- syntax sugar and the procedures to de-sugar it.

Understanding the essential idea that do with <- is indeed bind is necessary, as it prevents you from being confused by the imperative-like operation.

Explanation of a Simple Syntax Sugar Usage

The example below seems take a Maybe Int, add 1 to its value and put it back. It's the same as the imperative language.

wrong statement above.

The statement above is absolutely wrong because you MUSTN'T make an analogy with imperative languages. If you have done it already, you are falling into the trap of syntax sugar. With the wrong metaphor, how could you explain the v <- a when a is Nothing? This time, you cannot retrieve the value inside the Maybe because nothing contains such a value.

-- example1
add :: Maybe Int -> Maybe Int
add a = do
    v <- a
    return $ v+1

The correct understanding is the do notation is just a syntax sugar for the bind, so we can de-sugar the function add above without changing its functionality. There is nothing magic for the <- and do when encountering a Nothing because it is indeed a syntax sugar for >>= only.

-- de-sugar version of add
add :: Maybe Int -> Maybe Int
add a = a >>= \v -> return $ v+1

With the help of >>= definition of Maybe, it will return Nothing when the input is nothing, and plus 1 when there is a value.

Explanation of a Bit More Complex Syntax Sugar Usage

Let's see a bit more complex example. It seems multiples the values inside the values. However, after reading the example1, you could quickly know it's just a trick, which uses the do and <- syntax sugar to reduce the binding workflow.

-- example2
mul :: Maybe Int -> Maybe Int -> Maybe Int
mul a b = do
    x <- a
    y <- b
    return $ x*y

Hence, the de-sugared version code is:

mul :: Maybe Int -> Maybe Int -> Maybe Int
mul a b =
    a >>= \x ->
        b >>= \y ->
            return $ x*y

How to Understand the Closures While Binding

After de-sugaring the code in example2, the output code is not very easy to understand, at least in the beginning time when I learned it. This topic analyzes it with details. The problem with understanding the unfolded code is the x and y locates in different functions but they are used in the same function. To understand it, firstly, we look at this example:

example :: Int -> Int -> Int
example = \x -> \y -> x*y

In haskell, every function takes only one argument and then returns a function. There is no case that a function takes multiple arguments at once.

In this view, the example first takes x, then returns a function \y -> x*y, note that now the x is fixed because of currying. Nothing interesting, is it?

Given the example above, it's easy to understand how the de-sugared function mul works. It holds a lambda and then binds itself to the monad. Note the x is accessible in the returned lambda.

\x -> b >>= \y -> return $ x*y

Monad Construction and Values Inside It

Monad can hold some data that is not exposed and could be touched by certain functions only. This topic will explain how the monad is constructed and then clarify the type of constructor used in the monad.

Finally, we will see how the values are stored inside it and how we interact with them.

Type Constructor

There are two kinds of constructors: value constructor and type constructor. The value constructor is easy to understand as it creates a new object of a specific type. The type constructor creates a type. It neither defines a new type nor creates a new object of the specific type.

For example, the type constructor of the following code snippet is Tree:

data Tree a = Tip | Node a (Tree a) (Tree a)

Sometimes, the type constructor and value constructor overlap, for example, the Person is both a value constructor and a type constructor.

data Person = Person { name :: String, age :: Int, address :: String }

The importance of knowing the type constructor is we need to get familiar with the facade of some monad. Otherwise, reading no data with a type constructor is weird.

Construct A Monad by Return

As the topic above reveals, a type constructor creates a type only. It implies it's an interface and we cannot do concrete jobs on it, for example, retrieving values from it or converting it directly without the other functions. Moreover, it's impossible to construct a monad value according to a type constructor. Indeed, a type constructor is exported to avoid leaking the underlying details.

Hence, constructing a monad usually requires a return function. The return will constructor a monad according to the context as its signature is: return :: forall (m :: Type -> Type) a. Monad m => a -> m a

getMaybe :: Int -> Maybe Int
getMaybe = return

getReader :: Int -> Reader Int Int
getReader = return

All the functions above return a new monad, which takes the input value.

Execute the Monad Computation with Predefined Functions by Monad

You can see the Maybe monad holds data with Int type, which is exposed outside and we can bind it with another function. As it only contains a single data, it cannot adequately demonstrate the interactions of the data inside the monad and users, so let's watch State monad.

The State monad wraps a value with type a inside itself, which stands for a computation as a whole. The State monad allows the embedding of some states inside a computation and the definition of the State monad is:

newtype State s a = State { runState :: s -> (a, s) }

Now, the data with type a is held inside the State monad, and the state s comes from outside. Because of the constraint of newtype, the value with type a is inaccessible. To access the actual data inside it, runState function, which executes the computation of State, is required.

The output of the following code snippet is (3,4). The function runState executes the computation in a monad formation. The perception of monads are computations is significant, and to execute it, a predefined funtion from the monad is required.

Note that the value of s inside State monad is passed from outside during the execution.

Modify Data with Predefined Functions by Monad

As a monad is a computation and hasn't exposed its data, we need to rely on functions provided by a monad to interact with its data. For example, a Writer allows us to take a context along with a computation so we can modify it inside the computation.

factorial ::  Int -> Writer [Int] Int
factorial n
    | n <= 1 =
        tell [1] >>
        return 1
    | otherwise =
        factorial (n - 1) >>= \val ->
            let result = n * val
            in tell [result] >> return result

main :: IO ()
main =
    print $ runWriter $ factorial 5

The output is (120,[1,2,6,24,120]). The tell, which is defined by Writer monad, adds a new value to the monad. We will introduce the mechanism of values appending later.

>>= Flattens the Data in Monads When Binding

The signature of >>= reveals the type of result only. However, it lacks the important part of how the underlying data are resolved.

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

The understanding here is important as well. Otherwise, you will fall into considerable troubles when encountering >>, which is defined as below:

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

(>>) :: forall a b. m a -> m b -> m b
(>>) a b = a >>= \_ -> b

It doesn't reveal how the underlying data is handled, so let's try to >> on two IO ().

main :: IO ()
main =
    print "hello" >> print "world"
    -- synonym function call
    -- print "hello" >>= \_ -> print "world"

The code above will print helloworld, NOT world only. In IO () monad, the string to print is held inside the monad, which is inaccessible directly to users. However, after calling >>, the data is still preserved. It implies the >>= will merge the data inside m.

Further, let's continue to explain how the >> works to understand the flattening of >>=. In the example code of factorial above, when n<=2, it returns a Writer:

factorial n
    | n <= 1 =
        tell [1] >>
        return 1

This is a nice example to show how the values are flattened during binding. The tell [1] creates a new writer with a nonsense result. The return 1 creates another writer without context. The >> discards the input of tell [1] and then binds it to the return 1. As a result, the final writer is (1,[1]) after execution.

Flattening Convention of Bind in Monads

The monad is flattened as >>= is called. The convention depends on the implementation of the monad.

  • Maybe: Everything bound to Nothing will get Nothing.
  • State: the original state will be used and generate a new state, which is used to trigger the following binding function.
-- not the source code, but help to understand
-- from real workd haskell
-- file: ch14/State.hs
bindState :: State s a -> (a -> State s b) -> State s b
bindState m k = State $ \s -> let (a, s') = runState m s
                            in runState (k a) s'
  • Reader: ditto with State
  • Writer: the context will be flattened using mappend.

Category Theory Isn't Critical Here

Typically, understanding and using monads doesn't require you to understand the monad in category theory by any means. The category theory focuses on the condition when a group is a monad, and why it could be flattened without changing the original structure inside the monad. However, in haskell programming, no such understanding is required. What matters is the convention of flattening data while binding monads together.

Custom Monad: Focus on States Passing During Computation

Here we customize a monad to help understand the monad. Usually, the generic type a is required for a monad hence our customized monad is TestM a. Then we would like to use field history to see the history of applying and binding.

The core idea of the >>= is how two monads are squash together. (m (m b)) -> m b, which is guaranteed by the math foundation where we could ignore here.

The generic arguments a and b are not what we can manipulate among binding, because we cannot describe any concrete function to it during our implementation of the monad.

data TestM a = TestM
  { history :: String,
    state :: a
  }
  deriving (Show, Eq)

instance Functor TestM where
  fmap :: (a -> b) -> TestM a -> TestM b
  fmap f tm =
    TestM
      { state = f $ state tm,
        history = history tm ++ "f"
      }

instance Applicative TestM where
  pure :: a -> TestM a
  pure v = TestM {state = v, history = 0}
  liftA2 :: (a -> b -> c) -> TestM a -> TestM b -> TestM c
  liftA2 f v1 v2 =
    TestM
      { state = f (state v1) (state v2),
        history = history v1 ++ history v2 ++ "a"
      }

instance Monad TestM where
  (>>=) :: TestM a -> (a -> TestM b) -> TestM b
  (>>=) v f = do
    let result = f (state v)
    TestM
      { state = state result,
        history = history v ++ history result ++ "b"
      }

Besides the generic data a to b due to the binding of a -> m b, the left parts(history field data here) of the m should be conserved and hence be merged together according to the designer of the monad behavior. In this example, we decide to append them together with additional descriptions.

This is the same way when we try print "hello" >>= \_ -> print "world, the "hello" and "world" are the internal states of a IO () and will be merged together when we binding them together.