Table of Contents
What is a Monad?
A monad is a functional programming concept that essentially allows you to encapsulate the result of a function, with logic pertaining to how different result case’s should be treated. It is a way of composing functions that allow you to write code that is more readable and maintainable, when properly understood. Having said that, it is not a concept that is easy to understand, and can result in programmers not knowing how their implementation works. Their appearance can create confusion by obfuscating logic to places which are not immediately obvious. Once mastered, they are a useful form of encapsulating tedious detials of computational patterns, into concise functions.
Monads generally are used to simplify a program’s strucure, relying on abstraction to improve separation of concerns throughout the program. These abstractions create a more declarative style of programming, which is more readable and maintainable. If it is possible to remain agnostic about operational details, while still working with different underlying types, then a monad is a good candidate for the job. Conceptually, a monad is a type of functor, which is a type that maps a function over a collection of values. Having a strong understanding of the following concepts will help you understand monads:
If you are unfamiliar with monads but have come across the above concepts, it is likely that you have used a monad without realizing it. Below are some examples of monads that are commonly used in various programming languages:
- Maybe Monad
- Either Monad
- IO Monad
- Reader Monad
- Writer Monad
- State Monad
- List Monad
- Promise Monad
- Continuation Monad
Creating Monad’s in Typescript
Below we will explore how to create some of the above monads in typescript. There are 3 main components to a monad:
- A monadic type: 0, 1 or many types that are wrapped in the monad
- A unit operation: a function that takes a value of the monadic type and returns a monad
- A bind function or composition opertion: a function to call on each unit in the monad
Some monads/monad-like operations commonly used in typescript:
- Promise Monad
- State Monad in frameworks using React.js
Array.prototype.flatMap()
performs a monadic bind operation on an array of monadic types.
Maybe Monad
The maybe monad is an increasingly popular monad, as of late. I’ve personally heard
numerous statements how great rust
using is, and how it’s Option
type is
extremely useful for writing safe code. The Maybe
monad (also known as the
Option
monad), is a monad that is used to represent a value that may or may
not exist. In typescript, a very similiar pattern can be descrbed to the Maybe
monad is when using the optional chaining operator (?.
) on an object.
Essentailly, the Maybe
monad is a way of abstracting away the details of
handling when something might exist. This comes in handy when sequential
execution/function chained only happens when results are defined. Using the
maybe monad can be seen in the following example:
type None = null | undefined;
const isNone = (value: any): value is None =>
value === null || value === undefined;
class Maybe<T> {
constructor(private value: T | None) {}
static just<T>(value: T): Maybe<T> {
if (isNone(value)) return Maybe.nothing<T>();
return new Maybe<T>(value);
}
static nothing<T>(): Maybe<T> {
return new Maybe<T>(null);
}
static from<T>(value: T): Maybe<T> {
return Maybe.just(value);
}
public map<U>(f: (value: T) => U): Maybe<U> {
if (isNone(this.value)) return Maybe.nothing<U>();
return Maybe.just<U>(f(this.value));
}
public match<U>({
just,
nothing,
}: {
just: (value: T) => U;
nothing: () => U;
}): U {
if (isNone(this.value)) return nothing();
return just(this.value);
}
public flatMap<U>(f: (value: T) => Maybe<U>): Maybe<U> {
if (isNone(this.value)) return Maybe.nothing<U>();
return f(this.value);
}
}
// Maybe Monad Example
/**
* Output:
* Maybe { value: 16 }
*/
const hasResult: Maybe<number> = Maybe.from(10)
.map(x => x + 1)
.map(x => x + 2)
.map(x => x + 3);
console.log(hasResult);
/**
* Output:
* Maybe { value: null }
*/
const noResult: Maybe<number> = Maybe.from(10)
.map(x => null)
.map(x => x + 1);
console.log(noResult);
// Map vs FlatMap
/**
* Output:
* Maybe { value: Maybe { value: 2 } }
*/
console.log(Maybe.from(1).map(x => Maybe.from(x + 1)));
/**
* Output:
* Maybe { value: 2 }
*/
console.log(Maybe.from(1).flatMap(x => Maybe.from(x + 1)));
This is a very simple implementation of the Maybe
monad, but is a great
example about how can reduce the amount of boilerplate code that is required to
write safe code. In this example, the maybe
function is a simple function that takes a value
and returns it. The maybeMap
function is a function that takes a function and returns a function
Either Monad
The either monad is very similar to the
maybe monad, the key difference being Either<E, A>
requires two types to be defined.
Whereas Maybe<T>
, has two possible resulting types, but only requires one type in its
definition. On a high-level, Either
is just a more generic version of Maybe
, where
Maybe<T>
is logically equivalent to Either<None, T>
. In more detail, Either<E, A>
buids on wrapping
the types it is given, and using/matching the bind
function to chain operations together.
It can be used to represent computations that might fail. It usually has two types: Left (representing failure) and Right (representing success). Using the Either
monad in the below example,
requires specifing how to handle the Left
and Right
cases. This is done by using the bind
function, which takes a function that returns an Either
type.
export namespace EitherMonad {
export type Left<E> = { kind: "left"; value: E };
export type Right<A> = { kind: "right"; value: A };
export type Either<E, A> = Left<E> | Right<A>;
export function left<E>(value: E): Either<E, never> {
return { kind: "left", value };
}
export function right<A>(value: A): Either<never, A> {
return { kind: "right", value };
}
export function bind<E, A, B>(
m: Either<E, A>,
callbackfn: (_: A) => Either<E, B>
): Either<E, B> {
if (m.kind === "left") {
return m;
}
return callbackfn(m.value);
}
}
// Either Monad Example
function divByZero(x: number, y: number): EitherMonad.Either<number, number> {
if (y === 0) return EitherMonad.left(x);
return EitherMonad.right(x / y);
}
/**
* Output:
* { kind: 'left', value: 10 }
*/
console.log(divByZero(10, 0));
console.log(EitherMonad.bind(divByZero(10, 0), x => divByZero(x, 2)));
/**
* Output:
* { kind: 'right', value: 5 }
*/
console.log(divByZero(10, 2));
This example is not directly equivalent to the
Maybe
monad example, and is shown to demonstrate a simplistic representation of theEither
monad.
IO Monad
In layman terms, io monad is used to manage side effects on file operations (reading/writing/deleting). The core concept
describing an IO<A>
monad can be described as an action to be performed in the world,
optionally providing information about the world of type A
.
In a more technical sense, the IO<A>
monad represents a non-deterministic synchronous
computation that can cause side effects, yields a value of type A and never fails.
Reader Monad
Writer Monad
State Monad
This monad is used to model a single piece of muteable state S
.
The react hook useState
is a very commmon example of a state monad. The useState
hook sets
the initial state of a variable, and returns a function that can be used to update the state.
This is one method of implementing a state monad, but there are other ways to implement it.