You are already using monad and just don't know it. Simply using undefined
in Javascript is a monad. Let me show you an example in Typescript as types will make it more clear.
type Maybe<T> = T | undefined;
function maybeSix(): Maybe<number> {
if (Math.random() > 0.5) {
return 6;
}
return undefined;
}
function maybeDouble(maybeNumber: Maybe<number>): Maybe<number> {
if (maybeNumber !== undefined) {
return 2 * maybeNumber;
}
return undefined;
}
let maybeNumber: Maybe<number> = maybeSix();
const maybeDoubled: Maybe<number> = maybeDouble(maybeNumber);
console.log(maybeDoubled); // half the time 12, half the time undefined
The definition of a monand comes in 2 parts. First is to create a monad, another is to map a monad to another.
We "create the Maybe monad" by setting a variable of type Maybe<number>
with either a number
or undefined
. We "map the Maybe monad" with maybeDouble
.
We can write rewrite maybeDouble
to a more general form.
function mapMaybe<T, U>(maybe: Maybe<T>, map: (t: T) => Maybe<U>): Maybe<U> {
if (maybe !== undefined) {
return map(maybe);
}
return undefined;
}
function maybeDouble(maybeNumber: Maybe<number>): Maybe<number> {
return mapMaybe(maybeNumber, (number) => 2 * number); // number can no longer be null
}
What is the point of all this? We can see by using mapMaybe
, we don't need to do null-checks explicitly anymore. The null-check is implemented once in mapMaybe
and never again.
Another reason is to avoid the Pyramid of doom. Code without monads can get too nested.
const maybeNumber: Maybe<number> = ... // could be undefined
if (maybeNumber !== undefined) {
const maybeString: Maybe<string> = maybeNumber % 2 == 0
? String(maybeNumber)
: undefined;
if (maybeString !== undefined) {
const maybeBoolean: Maybe<boolean> = maybeString.length > 4
? true
: undefined;
if (maybeBoolean !== undefined) {
// 3 levels deep? Oh the humanity!
console.log(maybeBoolean);
}
}
}
But we can flatten the code with monads.
const maybeNumber: Maybe<number> = ... // could be undefined
const maybeString: Maybe<string> = mapMaybe(maybeNumber, (number) => maybeNumber % 2 == 0
? String(maybeNumber)
: undefined);
const maybeBoolean: Maybe<boolean> = mapMaybe(maybeString, (string) => maybeString.length > 4
? true
: undefined);
mapMaybe(maybeBoolean, (boolean) => console.log(boolean));
Once one understands monad is simply two operations (create and map), one can see monads in other parts of the code.
Promise
is a monad
// create a Promise monad
const promise = Promise.resolve(42);
// map a Promise monad
const doubledPromise = promise
.then((x) => 2 * x);
And since async/await
is just syntanic sugar over Promise
s, they too are monads. It is just less obvious at first.
async function foo() {
return 42;
}
async function doubleFoo() {
// A Promise monad is created when foo() is called
const x = await foo();
return 2 * x;
}
The Promise
is mapped using the async/await
keywords. Since doubleFoo
is an async
function, it returns a Promise
. async/await
made it easy to access the resolved value.
Do you want to to learn how to see more than just monads? You're in luck, Battlefy is hiring.