WTF is a Monad?!!
A monad is a monoid in the category of endofunctors. lol
Riki Phukon
· views
A monad in X is a monoid in the category of endofunctors of X.
I came across the term MONAD in a meme and when I googled. The above sentence was what I got. Not very helpful when you’re trying to learn about monads in functional programming isn’t it?
All monads have three components:
- Wrapper Type: A wrapper of some sorts that mark the type of the component.
- Wrap Function (allows entry to monad ecosystem): A function that takes normal values and wraps it up in a monad, like a constructor of sorts. These are called return, pure or unit
- Run Function (runs transformations on monadic values): Takes the wrapper type and transform function(not the Wrap Function) that accepts the unwrapped type and returns the wrapper type. Runs the transformation function on the argument passed in. aka bind, flatmap, '>>='' (this symbol)
Let’s start with some code
/**
* @see [GitHub](https://github.com/phukon/practice/tree/main/monadic-stuff)
*/
function square(x: number): number {
return x * x;
}
function addOne(x: number): number {
return x + 1;
}
console.log(addOne(square(2)));
The above code demonstrates basic arithmetic operations without using monads. We have simple functions for squaring a number and adding one to a number.
Let’s spice it up a bit
We undertake a substantial refactoring of the previous code, introducing a structured format that amalgamates the numerical output with an array of logs.
/**
* @see [GitHub](https://github.com/phukon/practice/tree/main/monadic-stuff)
*/
interface NumberWIthLog {
result: number;
logs: string[];
}
function square(x: number): NumberWIthLog {
return {
result: x * x,
logs: [`Squared ${x} to get ${x * x}`],
};
}
function addOne(x: NumberWIthLog): NumberWIthLog {
return {
result: x.result + 1,
logs: x.logs.concat([
`Added 1 to ${x.result} to get the result ${x.result + 1}`,
]),
};
}
console.log(addOne(square(3)));
This restructuring serves as a preparatory step towards implementing a monadic pattern, aimed at proficiently managing computations while concurrently logging operations. Key components within this file include:
- NumberWithLog Interface: This encapsulates both the result and log array, enhancing the code’s structural integrity.
Functions:
- square: Computes the square of a number and meticulously logs the operation.
- addOne: Increments a given NumberWithLog input by one, effectively documenting this process within the logs. The provided console log shows the usage of the addOne function on the square of 3.
But there’s a catch
When working with nested operations like square(square(2)) or addOne(5), we will encounter type mismatches and limitations within our code. To address these issue, we'll delve into implementing a more robust solution using monadic patterns.
The ‘NumberWithLog’ Interface
Firstly, we define an interface called NumberWithLog to contain both the numerical result and logs, crucial for our computations.
interface NumberWithLog {
result: number;
logs: string[];
}
Creating a Wrapper Function
We introduce a constructive function, wrapWithLogs, serving as a wrapper around a numerical value. It initializes a NumberWithLog object with the input number and an empty log array, priming it for subsequent operations.
/**
* Constructs a 'NumberWithLog' object by wrapping a numerical value.
* @param x The number to be wrapped within 'NumberWithLog'.
* @returns A 'NumberWithLog' object initialized with the input number and an empty log array.
*/
function wrapWithLogs(x: number): NumberWithLog {
return {
result: x,
logs: [],
};
}
Enhanced Functions for Operations
We enhance the square and addOne functions to operate on NumberWithLog objects. square computes the square of a NumberWithLog and meticulously records the operation in the logs. Similarly, addOne increments the result within a NumberWithLog object by one while logging the process.
square function
// Tweaked versions of square and addOne functions to operate on NumberWithLog
/**
* Computes the square of a 'NumberWithLog' and records the operation in logs.
* @param x The 'NumberWithLog' object whose result will be squared.
* @returns A 'NumberWithLog' object with the squared result and updated logs.
*/
function square(x: NumberWithLog): NumberWithLog {
return {
result: x.result * x.result,
logs: [`Squared ${x.result} to get ${x.result * x.result}`],
};
}
addOne function
/**
* Adds one to the 'NumberWithLog' result and logs the operation.
* @param x The 'NumberWithLog' object to which one will be added.
* @returns A 'NumberWithLog' object with the incremented result and updated logs.
*/
function addOne(x: NumberWithLog): NumberWithLog {
return {
result: x.result + 1,
logs: x.logs.concat([
`Added 1 to ${x.result} to get the result ${x.result + 1}`,
]),
};
}
Utilizing the Functions
To showcase the functionality, we display the result of square(square(wrapWithLogs(2))) in the console. This demonstrates the nested application of these functions and the effective handling of computations and logging within our codebase.
By leveraging these techniques, we resolve the type mismatch issues.
/**
* Issues:
* As evident, the previous implementation faces limitations with nested operations like square(square(2)) or addOne(5),
* resulting in type mismatches. We aim to resolve this issue with a new function that acts as a constructor-like wrapper.
* This wrapper function encapsulates the input within the 'NumberWithLog' structure to facilitate nested operations.
*
* @see [GitHub](https://github.com/phukon/practice/tree/main/monadic-stuff)
*/
// Interface to hold both the result and logs
interface NumberWithLog {
result: number;
logs: string[];
}
/**
* Constructs a 'NumberWithLog' object by wrapping a numerical value.
* @param x The number to be wrapped within 'NumberWithLog'.
* @returns A 'NumberWithLog' object initialized with the input number and an empty log array.
*/
function wrapWithLogs(x: number): NumberWithLog {
return {
result: x,
logs: [],
};
}
// Tweaked versions of square and addOne functions to operate on NumberWithLog
/**
* Computes the square of a 'NumberWithLog' and records the operation in logs.
* @param x The 'NumberWithLog' object whose result will be squared.
* @returns A 'NumberWithLog' object with the squared result and updated logs.
*/
function square(x: NumberWithLog): NumberWithLog {
return {
result: x.result * x.result,
logs: [`Squared ${x.result} to get ${x.result * x.result}`],
};
}
/**
* Adds one to the 'NumberWithLog' result and logs the operation.
* @param x The 'NumberWithLog' object to which one will be added.
* @returns A 'NumberWithLog' object with the incremented result and updated logs.
*/
function addOne(x: NumberWithLog): NumberWithLog {
return {
result: x.result + 1,
logs: x.logs.concat([
`Added 1 to ${x.result} to get the result ${x.result + 1}`,
]),
};
}
// Displaying the result of square(square(wrapWithLogs(2)))
console.log(addOne(square(wrapWithLogs(2))));
The Monad
We starts by addressing a challenge faced in prior implementations when dealing with nested operations like square(square(2)) or addOne(5). Attempting to modify square and addOne to accommodate ‘NumberWithLog’ inputs led to rendering these original functions obsolete.
The Monadic Revelation:
Enter ‘runWithLogs’, a game-changer in the landscape of this challenge. This higher-order function revolutionizes the approach without altering existing functions.
/**
* Executes a transformation on a 'NumberWithLog' object using the provided function
* and logs the operations performed.
* @param input The 'NumberWithLog' object to transform.
* @param transform The transformation function to apply to the 'NumberWithLog' object.
* @returns A 'NumberWithLog' object after applying the transformation and logging the operations.
*/
function runWithLogs(
input: NumberWithLog,
transform: (x: number) => NumberWithLog
): NumberWithLog {
const newNumberWithLog = transform(input.result);
return {
result: newNumberWithLog.result,
logs: input.logs.concat(newNumberWithLog.logs),
};
}
It gracefully accepts NumberWithLog objects and transformation functions, enabling seamless chaining of operations while retaining meticulous logs. This paradigm shift aligns with the monadic pattern, offering enhanced flexibility, maintainability, and a definitive resolution to type mismatch issues.
// Example usage of 'runWithLogs' function to chain operations on 'NumberWithLog' object
const a = wrapWithLogs(2);
const b = runWithLogs(a, addOne);
const c = runWithLogs(b, square);
console.log(c);
A simple console log demonstrates the prowess of ‘runWithLogs’, showcasing its ability to seamlessly chain ‘addOne’ and ‘square’ operations on a ‘NumberWithLog’ object initialized with a value of 2.
Mapping the components to the code in this tutorial
Remember I said that all monads have three components? This is how our code maps to the components :-
- Wrapper Type: NumberWithLog
- Wrap Function: function wrapWithLogs
- Run Function: function runWithLogs
Animation
If you look closely, Monads allow a user to chain operations while the Monad manages the secret work behind the scenes.
Footnotes:
A monad is a monoid in the category of endofunctors. Whats the problem? What is a Monad? — Computerphile The Absolute Best Intro to Monads For Software Engineers