A monad is a design pattern for writing code. Extremely powerful and approachable despite its complex math appearance.
Starting code
function square(x: number): number {
return x * x;
}
function addOne(x: number): number {
return x + 1;
}
calling addOne(square(2)) would return 5
if we wanted to return logs in each function (for transparency) the result would look something like this:
addOne(square(2)) => {
result: 5,
logs: [
"Squared 2 to get 4",
"Add 1 to 4 to get 5"
]
}
In order to support this pattern/logic we’ll define a return type:
interface NumberWithLogs {
result: number,
logs: string[]
}
Then update original functions to use NumberWithLogs return type:
function square(x: number): number {
return{
result: x * x,
logs: [
`Squared ${x} to get ${x * x}`
]
};
}
function addOne(x: number): number {
return {
result: x + 1,
logs: [
`Add 1 to ${x} to get ${x + 1}`
]
};
}
This is a good starting point for adding logs to the functions. More work needs to be done to allow the functions to append logs as calls are nested. We can use logic similar to [[Constructors]] to make this logic work seamlessly.
// Helps numbers ender the NumberWithLogs ecosystem
function wrapWithLogs(x: number): NumberWithLogs {
return {
result: x,
logs: []
}
}
// Now we just need to update the original functions to accept NumerWithLogs as input and make sure the return values are concatenating logs before returning
function square(x: NumberWithLogs): number {
return{
result: x.result * x.result,
logs: x.logs.concat([
`Squared ${x.result} to get ${x.result * x.result}`
])
};
}
function addOne(x: NumberWithLogs): number {
return {
result: x.result + 1,
logs: x.logs.concat([
`Add 1 to ${x.result} to get ${x.result + 1}`
])
};
}
Now we can call addOne(square(x)) or square(addOne(x)) and have the correct return value + associated logs.
New call pattern: addOne(square(wrapWithLogs(2)))
There is a decent amount of repeat code in the refactored functions, we can abstract away some of the logic to make it easier to read.
function square(x: NumberWithLogs): NumberWithLogs {
const newNumberWithLogs = {
result: x.result * x.result,
logs: [`Squared ${x.result} to get ${x.result * x.result}`]
}
return{
result: newNumberWithLogs.result,
logs: x.logs.concat(newNumberWithLogs.logs)
};
}
// now that we've refactored, lets create a helper function to make things even easier
// transform: function to apply to a number which returns a new NumberWithLogs object (this is where we can pass in square(), or addOne())
function runWithLogs(
input: NumberWithLogs,
transform: (_: number) => NumberWithLogs
): NumberWithLogs {
const newNumberWithLogs = transform(input.result)
return {
result: newNumberWithLogs.result,
logs: input.logs.contat(newNumberWithLogs.logs)
}
}
// because of this helper function, we no longer need addOne or Square to do log concatenation anymore.
function square(x: number): NumberWithLogs{
return {
result: x * x,
logs: [`Squared ${x.result} to get ${x.result * x.result}`]
}
}
function addOne(x: number): NumberWithLogs{
return {
result: x + 1,
logs: [`Add 1 to ${x.result} to get ${x.result + 1}`]
}
}
Now we can call the functions in any order we want, get the associated logs for the transformations, and extend this logic to any future functions we introduce.
// Example usage
const a = wrapWithLogs(5)
const b = runWithLogs(a, square)
const c = runWithLogs(b, addOne)
This centralized logic + functions is a Monad!
What is a monad?
A design pattern that allows you to chain operations while managing busy work (concatenating of logs) in a centralized location.
All monads have 3 components:
- Wrapper type: a wrapper of some sort that determines the type of the monad (
NumberWithLogsfrom example above) - Wrap function: a function that takes normal (primitive) values and transforms them into the wrapper type (
wrapWithLogsfrom example above). This is sometimes calledReturn,Pure, orUnit. - Run function: a function that accepts a wrapper type and a transform function (function that accepts an unwrapped type and returns the wrapper type). This is sometimes called
Bind,flatMapor>>=. In the example above, this wasrunWithLogs
Why use monads?
They allow for one-time definition of more-complex logic to be handled behind the scenes.
Common uses for monads
- Optional generic type
<T>(abstracts away possibility of empty values) - NumberWithLogs / Writer (abstracts away accumulation of log data)
- Future/promise (abstracts away possibility for values to only become available later)
- List (abstract away idea of branching computation)
- The js
map()function treats lists as a monad to apply an action to the entire array, then flattening all the resulting array into a single list.
- The js