Monads
Introduction
Heard about this with a friend and never really revisited it. With my attention at home on Rust, thought I might write something down
The Maybe Monad
Well possibly the Option monad for me. To make one of these you need define something to wrap your thing with and a function which could fail that returns the wrapper type
return :: a => Maybe a
>>= :: Maybe a
This was explained to me a little better with this picture.

What's the Point
- Same idea works for other effects, e.g. reading from environments, input/output
- Supports pure programming with effects
- Use of effects explicit in types
- Functions that work for any effect
Second Time Through
Really want to get this into my head. It is another Mathy thing that just requires me to have a light bulb moment.
Step 1 - Starting Code
So the YouTube started off with this which I might need to change
function square(x: number): number {
return x * x;
}
function addOne(x: number): number {
return x + 1;
}
// Which allows us to chain the two together. e.g.
addOne(square(2)) => 5
// I think this is the goal. To do something extra which uses the input data
// This example confused me because it is not valid typescript
addOne(square(2)) => {
result: 5,
logs: [
"square(2) => 4",
"addOne(4) => 5"
]
}
I think what they were suggest is they want the two functions to return an object with logs e.g.
const result = addOneWithLogs(squareWithLogs(2))
// Where result is
{
result: 5,
logs: [
"square(2) => 4",
"addOne(4) => 5"
]
}
Step 2 - Implement new Funcs
First we define an interface to hold the result we are after. i.e. the result and logs
interface NumberWithLogs {
result: number;
logs: string[];
}
Now we make our new addOneWithLogs and squareWithLogs to get our result
function squareWithLogs(x: number): NumberWithLogs {
const result = x * x;
return {
result,
logs: [`Squared ${x} to get ${result}`]
};
}
function addOneWithLogs(x: NumberWithLogs): NumberWithLogs {
const result = x.result + 1
return {
result,
logs: x.logs.concat([
`Added one to ${x.result} to get ${x.result + 1}`
])
};
}
const result2 = addOneWithLogs(squareWithLogs(2));
// Gives
{
result: 5,
logs: [ 'Squared 2 to get 4', 'Added one to 4 to get 5' ]
}
Step 3 - Identifying real Requirements
They want to be able to do this
squareWithLogs(squareWithLogs(2))
addOneWithLogs(5)
This is not possible with the current implementation as addOneWithLogs and squareWithLogs functions both take a ``NumberWithLogs``` as an argument not a ```number```
Step 4 - Wrapping the arguments
The next thing we do is to create a function which does take a number and converts it to a NumberWithLogs so we can call squareWithLogs(squareWithLogs(2)) and addOneWithLogs(5)
function wrapWithLogs(x: number): NumberWithLogs {
return {
result: x,
logs: []
};
}
We can now looking at squareWithLogs we can change the argument from number to squareWithLogs
function squareWithLogs(x: NumberWithLogs): NumberWithLogs {
const result = x.result * x.result;
return {
result,
logs: x.logs.concat([
`Squared ${x.result} to get ${result}`
])
};
}
Now we can call squareWithLogs
const result = squareWithLogs(squareWithLogs(wrapWithLogs(2)))
// Gives
{ result: 16, logs: [ 'Squared 2 to get 4', 'Squared 4 to get 16' ] }
Step 5 - Reviewing Code
We need to look at our code for duplication.
Original Code No Change
Here is the original code
function addOneWithLogs(x: NumberWithLogs): NumberWithLogs {
const result = x.result + 1
return {
result,
logs: x.logs.concat([
`Added one to ${x.result} to get ${x.result + 1}`
])
};
}
function squareWithLogs(x: NumberWithLogs): NumberWithLogs {
const result = x.result * x.result;
return {
result,
logs: x.logs.concat([
`Squared ${x.result} to get ${result}`
])
};
}
Rework Version 1
Let's change the return on both to calculate the logs outside of the return then the return function will look the same.
function addOneWithLogs(x: NumberWithLogs): NumberWithLogs {
const result = x.result + 1
const thisLogs = [
`Added one to ${x.result} to get ${result}`
];
return {
result,
logs: x.logs.concat(thisLogs)
};
}
function squareWithLogs(x: NumberWithLogs): NumberWithLogs {
const result = x.result * x.result;
const thisLogs = [
`Squared ${x.result} to get ${result}`
];
return {
result,
logs: x.logs.concat(thisLogs)
};
}
Rework Version 2
Now lets wrap the top half of both in a NumberWithLogs. Notice the return is the same for both
function addOneWithLogs(x: NumberWithLogs): NumberWithLogs {
const numberWithLogs: NumberWithLogs = {
result: x.result + 1,
logs: [
`Added one to ${x.result} to get ${x.result + 1}`
]
}
return {
result: numberWithLogs.result,
logs: x.logs.concat(numberWithLogs.logs)
};
}
function squareWithLogs(x: NumberWithLogs): NumberWithLogs {
const numberWithLogs: NumberWithLogs = {
result: x.result * x.result,
logs: [
`Squared ${x.result} to get ${x.result * x.result}`
]
}
return {
result: numberWithLogs.result,
logs: x.logs.concat(numberWithLogs.logs)
};
}
Step 5 - The NumberWithLogs Transform function
So looking at both functions we can see the top half is a function which takes x as an argument and returns a NumberWithLogs. e.g.
const foo = (x: NumberWithLogs):NumberWithLogs => {
....
}
This is called a transform and in our case it might be addOne or square. Either way we start with a NumberWithLogs and return a NumberWithLogs. So lets express these as squareTransform and addOneTransform
const squareTransform = (x: NumberWithLogs): NumberWithLogs => {
return {
result: x.result * x.result,
logs: [
`Squared ${x.result} to get ${x.result * x.result}`
]
}
}
const addOneTransform = (x: NumberWithLogs): NumberWithLogs => {
return {
result: x.result + 1,
logs: [
`Added one to ${x.result} to get ${x.result + 1}`
]
}
}
Step 6 - The n Transform function
We can see above we do not reference the x.logs in the transform so we can simplify these transforms to just take a number and they will work the same way.
const squareTransform = (x: number): NumberWithLogs => {
const result = x * x;
return {
result,
logs: [`Squared ${x} to get ${result}`]
}
}
const addOneTransform = (x: number): NumberWithLogs => {
const result = x + 1;
return {
result,
logs: [`Added one to ${x} to get ${result}`]
}
}
Step 6 - The RunWithLogs function
Now we can run either of these functions with this
const runWithLogs = (
input: NumberWithLogs,
transform: (x: number) => NumberWithLogs): NumberWithLogs => {
const numberWithLogs = transform(input.result);
return {
result: numberWithLogs.result,
logs: input.logs.concat(numberWithLogs.logs)
}
}
🧭 Conclusion: My First Monad
In exploring monads, I built a simple wrapper called NumberWithLogs that tracks both a result and a log of operations. This helped me understand how monads let us:
- Wrap values with extra context (like logs)
- Chain operations while keeping that context
- Keep the code readable and composable
🧱 Monad Building Blocks (Simplified)
| Concept | Simple Explanation | My Example | Also Known As |
|---|---|---|---|
| Wrapper Type | A container that holds a value and extra info (like logs) | NumberWithLogs
|
— |
| Wrap Function | Takes a plain value and puts it into the container | wrapWithLogs
|
return, pure, unit
|
| Run Function | Applies a transformation to the value, keeping the container structure | runWithLogs
|
bind, flatMap, >>=
|
🧪 Transform Functions I Used
Each function takes a number and returns a NumberWithLogs — a result plus a log message.
const squareTransform = (x: number): NumberWithLogs => {
const result = x * x;
return {
result,
logs: [`Squared ${x} to get ${result}`]
};
};
const addOneTransform = (x: number): NumberWithLogs => {
const result = x + 1;
return {
result,
logs: [`Added one to ${x} to get ${result}`]
};
};
🧰 Monad Functions
function wrapWithLogs(x: number): NumberWithLogs {
return {
result: x,
logs: []
};
}
const runWithLogs = (
input: NumberWithLogs,
transform: (x: number) => NumberWithLogs
): NumberWithLogs => {
const numberWithLogs = transform(input.result);
return {
result: numberWithLogs.result,
logs: input.logs.concat(numberWithLogs.logs)
};
};
🧠 What I Learned
- Monads help manage extra context (like logs) without cluttering logic
- They make chaining operations predictable and clean
- Even simple wrappers can teach deep ideas about functional design
Finally
The YouTube left us with this slide. I guess the most obvious ones are Option, Result, Writer, Future/Promise
