A Comprehensive Guide to Monads

Finally breaking down “What on Earth is a Monad?”

Nishant Aanjaney Jalan
CodeX
5 min readDec 22, 2023

--

A monad is a function composition technique that externalizes treatment for some input scenarios using a composing function, bind, to pre-process input during composition. — some StackOverflow user

In other words, A monad is a design pattern which abstracts away some internal processes. You actually see a lot of these patterns quite regularly in many programming languages.

Source

Defining a Monad in Typescript

function multiplyNumbers(a?: number, b?: number): number | undefined {
if (!a) return undefined
if (!b) return undefined
return a * b
}

console.log(multiplyNumbers(3, 4)) // 12
console.log(multiplyNumbers(5)) // undefined

If my code is correct, that is the expected behaviour. The above seems like an everyday situation in your career. However, this needs to be more scalable. There may be multiple of these situations where you need to return undefined if something turns out to be undefined.

Let us create a type that can hold a particular value.

class Nullable<Type> {
value?: Type

constructor(value?: Type) {
this.value = value
}

isAbsent(): boolean {
return this.type == undefined
}

lift(): Type {
if (this.isAbsent)
throw new Error("Cannot lift from empty Nullable");
return this.value;
}
}

function multiplyNumbers(
a: Nullable<number>,
b: Nullable<number>
): Nullable<number> {
if (a.isAbsent) return new Nullable();
if (b.isAbsent) return new Nullable();
return new Nullable(a.lift() * b.lift());
}

console.log(multiplyNumbers(new Nullable(3), new Nullable(4)).lift());

Yuck, we wrote so much code that can be done with so little. You might wonder why but bear with me here. Notice that there exists a code duplication: if (something.isAbsent) return new Nullable(). Based on the type that we are working on, it is quite obvious that this is the expected behaviour of the function. Is there a way we can abstract out that information?

The main idea is to introduce a function that can bind multiple functions together:

function bindNullable<Type, TType>(
nullable: Nullable<Type>,
transform: (_: Type) => Nullable<TType>
): Nullable<TType> {
if (nullable.isAbsent)
return new Nullable();
return transform(nullable.lift());
}

This is the abstracted logic in the bindNullable function. We are checking if the nullable object has a value. If it doesn’t, return something empty (new Nullable), else it will apply the transform (mapping) function to the value inside the Nullable object and return its result.

Now we can rewrite our multiplyNumbers function as:

class Nullable<Type> {
value?: Type

constructor(value?: Type) {
this.value = value
}

isAbsent(): boolean {
return this.type == undefined
}

lift(): Type {
if (this.isAbsent)
throw new Error("Cannot lift from empty Nullable");
return this.value;
}
}

function bindNullable<Type>(
nullable: Nullable<Type>,
transform: (_: Type) => Nullable<Type>
): Nullable<Type> {
if (nullable.isAbsent)
return new Nullable();
return transform(nullable.lift());
}

function multiplyNumbers(
a: Nullable<number>,
b: Nullable<number>
): Nullable<number> {
return bindNullable(a, aVal =>
bindNullable(b, bVal => new Nullable(aVal * bVal))
)
}

console.log(multiplyNumbers(new Nullable(3), new Nullable(4)).lift());

If you focus on just the multiplyNumbers function, this has become a very simple function that does exactly what we need to do! Imagine that you have multiple Nullable values and you want to return undefined if one of the operations resulted in a null. This is a great way to abstract out some underlying logic in your code. That is a Monad. As said at the beginning of this article, it is a design pattern.

How Haskell uses Monads

data Maybe a = Nothing | Just a -- already defined in Prelude

-- Our code here
(*) :: Maybe Int -> Maybe Int -> Maybe Int
a * b = case a of
Nothing -> Nothing
Just aVal -> case b of
Nothing -> Nothing
Just bVal -> Just (aVal * bVal)

This Haskell function defined above is the same code as the Typescript one shown above. Looks ugly, right? Fortunately, Maybe in Haskell is already defined as a Monad for us!

-- already defined in Prelude
class Applicative m => Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b

instance Monad Maybe where
return a = Just a
a >>= f = case a of
Nothing -> Nothing
Just aVal -> Just (f aVal)

If you are not familiar with Haskell, return is a function that converts a type from the normal world to its Monadic world. The return function type tells us “Give me a value, and I will return you that exact value in my world”. In our typescript example, that was the new Nullable(a).

If you take a closer look at the types, you will see that (>>=) is the same as the bindNullable function. In fact in Haskell, (>>=) is called the bind operator. This takes two parameters, a value in the Monadic world. The value inside the monad is then transformed with a function and the result is the result of applying that function to the value inside. How does this help us in Haskell?

(*) :: Maybe Int -> Maybe Int -> Maybe Int
a * b = a >>= (\aVal ->
b >>= (\bVal ->
return (a * b)
)
)

This function looks a lot cleaner than before. Note: Do not get confused by the return here. This is not your normal return keyword (Haskell has no return keyword). return is a function that takes in a value and returns the Monadic world version of it — similar to new Nullable(aVal * bVal).

Haskell has a special way of dealing with this. Using the >>= operator in chains does not look very eye-pleasing although it does its job. There’s a special way to write this that does not change its meaning or function:

(*) :: Maybe Int -> Maybe Int -> Maybe Int
a * b = do
aVal <- a
bVal <- b
return (aVal * bVal)

Now that’s a better way to write this.

Other languages that use this Monad

Haskell isn’t the only language that comes with this monadic type. Take Rust for example:

fn multiplyNumbers(a: Option<i32>, b: Option<i32>) -> Option<i32> {
let aVal = a?;
let bVal = b?;
return Option::Some(aVal * bVal)
}

Or even Kotlin:

fun multiplyNumbers(a: Int?, b: Int?): Int? {
val bVal = b ?: return null
return a?.times(bVal)
}

Granted that Kotlin does not have an implicit function type that deals with returning null by default, it can mimic the use of Monads in some way.

Conclusion

Monads are usually tossed around as this buzzword that people don’t understand what it means. The core concept of this is to abstract some very common code that is dealt with behind the scenes so that it does not interfere with the actual code.

I hope you enjoyed reading my article and learned something. Thank you!

Want to connect?

My GitHub profile.
My Portfolio website.

--

--

Nishant Aanjaney Jalan
CodeX
Editor for

Undergraduate Student | CS and Math Teacher | Android & Full-Stack Developer | Oracle Certified Java Programmer | https://cybercoder-naj.github.io