It took me a lot of struggling to understand what exactly is a monad. It’s been explained everywhere in terms of either its formulas or in long articles that were almost impossible to comprehend. At its core, monad is a generic concept that helps with operations between pure functions to deal with side effects.

What Is a Monad?

A monad is a beautiful and generic way of handling side effects in pure functions that provides a scalable approach for composing pure functions by using bind and unit concepts.

Here, I’ll be explaining the complex logic in simple words. Also, in case you are interested in watching a video for this topic, do check out the video below.

Also, I’ll be coding in Python so that Haskell’s syntax doesn’t scare you away.

 

Functional Programming and Monads Explained

Before starting, let me first introduce you to the main feature of Haskell, functional programming, which is the reason monads are needed in the first place. Functional programming is a mindset in which all design is thought of in terms of pure functions. There are two key concepts you need to understand: 

  • Functions are first-class citizens. So, all simple logic is a function, and all complex logic is handled by doing operations between functions.
  • Functions have to be pure. This means whatever input you give, the output should always remain the same. The only way to interact with pure functions is via input and output only. It can’t access global states, print anything or even throw exceptions until defined in the function definition.

As I explained earlier, a monad is a general concept that helps with operations between pure functions deal with the side effects. 

To help you understand what that means, I’ll explain what a monad is using an example, and it will become clear in a jiffy.

 

How a Monad Works

Problem Statement

Let’s start with doing multiple times square on a number.

def square(num: int) -> int:
    return num * num;
print(square(square(2)));
--------------------------------------------------------------------
Output
16

More on Python: 5 Types of Arguments in Python Function Definitions

 

Introducing Side Effects

Now, let’s add a side-effect to the function by printing the current value of input as well.

def sqaure_with_print(num: int) -> int:
    print("Currrent num: ", num);
    return num * num;
print(sqaure_with_print(sqaure_with_print(2)));
--------------------------------------------------------------------
Output
Currrent num:  2
Currrent num:  4
16

 

Pure Functions With Side Effects

Due to the introduction of the print statement, the above function is no longer a pure function. What do we do now? How do we handle side-effects in a pure function? Remember, the only way to interact with a pure function is via input and output. We need to get the logs in the output itself.

def sqaure_with_print_return(num: int) -> (int, str):
    logs = "Currrent num " + str(num);
    return (num * num, logs);
print(sqaure_with_print_return(sqaure_with_print_return(2)));
--------------------------------------------------------------------
Output
Traceback (most recent call last):
File "hello.py", line 21, in <module>
print(sqaure_with_print_return(sqaure_with_print_return(2)));
File "hello.py", line 17, in sqaure_with_print_return
return (num * num, logs);
TypeError: can't multiply sequence by non-int of type 'tuple'
A tutorial on the basics of a monad. | Video: Computerphile

More on Python: Python Class Inheritance Explained

 

Custom Composing

Oops. While we were able to make it a pure function, in order to do the chaining of the functions along with outputting, the side-effects broke the program. Why did this happen? The function was expecting int and we passed (int, str). Looks like it was just an expectation mismatch. Looks like some special handling needs to be done.

We might need to modify square_with_print_returnsuch that it can accept (int, str) rather than int. Should we change the input signature of a function, just so that they can combine? Will that be scalable?

Instead, let’s add a custom compose function, which knows how to handle the above parameter mismatch.

def sqaure_with_print_return(num: int) -> (int, str):
    logs = "Currrent num " + str(num);
    return (num * num, logs);
def compose(func2, func1, num: int):
    res1 = func1(num)
    res2 = func2(res1[0])
    return (res2[0], res1[1] + res2[1]);
print(compose(sqaure_with_print_return, sqaure_with_print_return, 2));
--------------------------------------------------------------------
Output
(16, 'Currrent num 2 Currrent num 4')

It worked. Now, let’s say we want to chain three functions. We’ll have to rewrite this compose function, right? We need to find a scalable way of writing this function so that it can handle any number of functions.

 

Wrapping Pure Functions

A better solution is to use other wrapper functions, which can help with handling the input and output parameters in the desired format. This function will know how to handle side-effects without changing the pure function’s parameter.

from typing import Tuple
def sqaure_with_print_return(num: int) -> (int, str):
    logs = "Currrent num " + str(num);
    return (num * num, logs);
def bind(func, tuple: Tuple[int, str]):
   res = func(tuple[0])
   return (res[0], tuple[1] + res[1])
def unit(number: int):
   return (number, "");
print(bind(sqaure_with_print_return, (bind(sqaure_with_print_return, unit(2)))))
--------------------------------------------------------------------
Output
(16, 'Currrent num 2 Currrent num 4')
  • Bind function: This is a wrapper function on square_with_print_returnthat accepts a tuple, rather than just int. So, all such functions can be wrapped with a bind function. Because of this, now your pure function can handle any kind of input parameters.
  • Unit function: Our first parameter was an integer. Someone should convert that to a tuple as well, right? That’s where the unit function helps.

And this my friend, is a monad.

Concludingly, we can say that Monads are just a beautiful and generic way of handling side effects in pure functions and provide a scalable approach for composing pure functions by using bind and unit concepts.

Expert Contributors

Built In’s expert contributor network publishes thoughtful, solutions-oriented stories written by innovative tech professionals. It is the tech industry’s definitive destination for sharing compelling, first-person accounts of problem-solving on the road to innovation.

Learn More

Great Companies Need Great People. That's Where We Come In.

Recruit With Us