wtf is a MONAD?!
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, >>=
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 givenNumberWithLog
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))));
// Export to avoid 'Duplicate function implementation' warning by Typescript
export {};
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:
Subscribe to my newsletter
Read articles from Riki Phukon directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Riki Phukon
Riki Phukon
Love ham radios, and computers. My works span web, CLI apps, blogs, and tools aimed at aiding fellow engineers.