What is a Monad in Haskell?

2019-11-26

The first time I heard about Monad was at a Scala Meetup. Later, I tried to understand the concept of Monad but was overwhelmed by various voluminous books and tutorials on Haskell. Then I came across Ruan Yifeng’s article “Illustrated Monad“ published in 2015. Although it was clear and easy to understand, it was detached from Haskell, and the illustrations didn’t match the concepts in the language. Ruan Yifeng’s article was translated from “Functors, Applicatives, And Monads In Pictures,” which I read through.

Introduction

Computer programs are used to control a computer to perform calculations. The objects operated on by programs are various types of values, such as numerical values. Here’s a simple value 2:

Using a function to process the value can return the result of the function execution, such as:

Apart from simple numerical types, values can also be contained within some contextual environment, forming more complex value types. You can think of the contextual environment as a box, with the numerical value placed inside the box. This box as a whole is described as Just 2, which is a boxed 2:

If you are familiar with Java, you can think of this box as a wrapper class, like Integer and int, corresponding to boxed and unboxed 2.

Functors

Facing a boxed 2, we can’t directly apply the +3 function to it:

At this point, we need a function fmap to operate on it. fmap will first extract the value 2 from Just 2, then add 3 to it, put the result 5 back into the box, and return Just 5:

How does fmap know how to parse Just? What if it was another type like Only, could it still parse it? That’s why Functor is needed to complete the definition of this operation.

A Functor is a type of data type:

Functor defines the behavior of fmap:

fmap has two input parameters and one output parameter. The input parameters are a function and a boxed value, and the output parameter is a boxed value. It can be used like this:

fmap (+3) (Just 2)
-- Just 5

Returning to Haskell, the Haskell “system library” has an instance of Functor called Maybe, which defines the behavior of fmap, specifying how to operate on values when the input parameter is of type Just:

instance Functor Maybe where
  fmap func (Just val) = Just (func val)
  fmap func Nothing = Nothing

The entire process of the expression fmap (+3) (Just 2) is similar to this:

Similarly, from the definition of Maybe, it can be seen that if the second parameter passed to fmap is Nothing, the function will return Nothing. This is indeed the case:

fmap (+3) Nothing
-- Nothing

Now suppose a scenario in Java where a user uses a utility class Request to make a request to a server, and the request returns a type Response. Response is an entity class that may or may not contain the required data:

Response res = Request.get(url);
if (res.get("data") != null) {
  return res.data;
} else {
  return null;
}

In Haskell, using fmap would become:

fmap (get("data")) (Response res)

Of course, Haskell doesn’t have a get("data") syntax. You can encapsulate the operation of getting Response.data as a function getData, then pass it into fmap as the first parameter.

Haskell provides a syntax sugar <$> for fmap to simplify its usage:

getData <$> (Response res)

Next, think about how Haskell functions operate on lists. The function performs calculations on each element of the list and then returns a list:

In fact, lists are also Functors. Here is the definition of lists:

instance Functor [] where
  fmap = map

Applicatives

Applicatives is another concept. We previously said that values are placed in boxes. What if functions are also placed in boxes?

Haskell provides the operator <*> to handle functions inside boxes:

For example:

Just (+3) <*> Just 2 == Just 5

Using <*> can also accomplish some interesting operations, such as multiplying and adding each element in a list:

[(*2), (+3)] <*> [1, 2, 3]
-- [2, 4, 6, 4, 5, 6]

Monads

Function execution involves processing values using functions with arguments, involving three roles. Functors place the processed value in a box, Applicatives place the function in a box, and Monads place the function’s argument in a box. Monads have an operator >>= to achieve Monad functionality. Suppose there is a function half that takes a numerical value as an argument. If the value is even, it divides by 2; otherwise, it returns Nothing:

half x = if even x
  then Just (x `div` 2)
  else Nothing

How do you pass a Just type value to half?

>>= can solve this problem:

Just 3 >>= half
-- Nothing

The >>= operator takes Just 3, transforms it into 3, and processes it in half. A Monad is a data type that defines the behavior of >>=:

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

Here, Maybe is a Monad (existing alongside the Maybe mentioned above):

instance Monad Maybe where
  Nothing >>= func = Nothing
  Just val >>= func = func val

>>= also supports chain operations:

Just 20 >>= half >>= half >>= half
-- Nothing

Summary

Although Haskell’s Monad is quite famous, it actually involves three concepts: Functors, Applicatives, and Monads. Perhaps Monad has more extensive applications. In data processing, FP is not superior to OOP; the logic is similar, only the writing style differs. Facing the same problem with different thinking and expressions corresponds to different programming ideas and paradigms. There are many intricate theories in the world waiting for us to explore.