Practically explain Monads in plain TypeScript
Programming by composing pure functions makes life easier, but real-world applications often depend on side effects. Functors and Monads provide ways of composing side effects operations within pure function chaining. This post provides a practical example to explain Monads in plain TypeScript.
import * as readline from "readline";
// ------------------------------------------------------
interface Functor<T> {
// (a -> b) -> f a -> f b
fmap<U>(func: (val: T) => U): Functor<U>;
}
interface Monad<T> {
// (a -> m b) -> m b
bind<U>(mona: (val: T) => Monad<U>): Monad<U>;
}
class IO<T> implements Functor<T>, Monad<T> {
private constructor(private effect: () => T) {}
static of<V>(effect: () => V): IO<V> {
return new IO(effect);
}
run(): T {
return this.effect();
}
fmap<U>(func: (val: T) => U): IO<U> {
return IO.of(() => func(this.run()));
}
bind<U>(mona: (val: T) => IO<U>): IO<U> {
return IO.of(() => mona(this.run()).run());
}
}
// ------------------------------------------------------
const ri = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const ioLogResult = (p: Promise<[number, boolean]>): IO<Promise<void>> =>
IO.of(() =>
p.then(([tgt, result]) =>
ri.write(`${result ? "correct" : "wrong"}, the ans is ${tgt}`)
)
);
const ioReadNum = (prompt: string): IO<Promise<number>> =>
IO.of(
() =>
new Promise<number>((resolve) => {
ri.question(prompt, (ans) => resolve(parseInt(ans)));
})
);
const ioReadNumAndCompare = (
tgt: number,
prompt: string
): IO<Promise<[number, boolean]>> =>
ioReadNum(prompt).fmap((pVal) => pVal.then((val) => [tgt, val === tgt]));
const ioRand = IO.of(() => Math.floor(Math.random() * 3) + 1);
const prompt = "guess a num between 1-3\n";
// ---- pure functions chained without side effects -----
const expr = ioRand // IO<number>
.bind((tgt) => ioReadNumAndCompare(tgt, prompt)) // IO<Promise<boolean>>
.bind(ioLogResult); // IO<Promise<void>>
// expr is a pure expression without any actually IO action
// ------------------------------------------------------
// guess a num between 1-3
// 3
// correct, the ans is 3
expr.run().finally(() => {
ri.close();
});