UPDATED BY
Brennan Whitfield | Jul 20, 2023

A monad is a generic concept that helps streamline operations between pure functions in functional programming, often to handle side effects.

Monad Definition

A monad is a generic structure in functional programming that handles side effects in pure functions. It provides a scalable approach for composing pure functions by using bind and unit concepts.

Here, I’ll explain the complex logic in simple words. Also, I’ll code 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 to deal with side effects (which is when a function changes a variable that’s non-local or outside of its scope).

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.

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

 

How a Monad Works in Functional Programming (With a Monad Example)

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

 

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("Current num: ", num);
    return num * num;
print(sqaure_with_print(sqaure_with_print(2)));
--------------------------------------------------------------------
Output
Current num:  2
Current 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 = "Current 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'

 

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_return, such 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 = "Current 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, 'Current num 2 Current 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 = "Current 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, 'Current num 2 Current num 4')
  • Bind function: This is a wrapper function on square_with_print_return that 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.

More on Python: Python Class Inheritance Explained

 

Applications of Monads

Monads allow users to simplify structure and control flows in functional programming, helping make code more declarative, type-safe and clear to follow. As monads are effective at isolating and containing side effects, this makes them useful for solving input/output (I/O) errors, parsing a program’s state and exception handling. They can also wrap values with extra data that would otherwise be inaccessible to functions, and facilitate calling functions under certain conditions.

A tutorial on the basics of a monad. | Video: Computerphile

 

History of Monad

The concept of a monad was introduced in 1958 by mathematician Roger Godement in the context of category theory, a theory in mathematics concerning how different mathematical structures are related to one another. The actual term of “monad” became coined and popularized later by mathematician Saunders Mac Lane.

Monads were first related to functional programming in 1989 by computer scientist Eugenio Moggi. Moggi applied monads as part of semantics to interpret lambda calculus, a formal model of expressing computation that uses variable binding and substitution for function abstraction and application. 

This use for monads was expanded upon to apply to functional programming, being utilized in programming languages like Haskell to perform I/O operations. Since Haskell, monad patterns are now used in common programming languages like Python, JavaScript and Scala.

Monads, as we can see, 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