Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Can we treat Promises as Monads?
// Proposition: Promise is equivalent to a loosely typed `ExceptT e IO a` for
// javascript, and fundamentally obeying the monad laws, making it a
// helpful construction for writing computations.
// class Monad m where
// return :: a -> m a
// (>>=) :: m a -> (a -> m b) -> m b
//
// Monad laws:
// Left identity: return a >>= f ≡ f a
// Right identity: m >>= return ≡ m
// Associativity: (m >>= g) >>= f ≡ m >>= (\x -> g x >>= f)
// Definitions:
//
const _return = <A extends any>(x: A): Promise<A> => Promise.resolve(x);
const _right = _return;
const _left = (e: any) => Promise.reject(e);
const _bind = <A extends any, B extends any>(p: Promise<A>, f: ((x: A) => Promise<B>)) => p.then(f);
const _withLeft = <E extends any, B extends any>(p: Promise<any>, f: ((e: E) => Promise<B>)) => p.catch(f);
// Helpers for examples
//
async function assertEq(p1: Promise<any>, p2: Promise<any>) {
const [a, b] = await Promise.all([p1, p2]);
if(a !== b) {
throw new Error(`Assertion Failure: ${a} /= ${b}`);
}
}
async function seq(xs: (() => Promise<any>)[]) {
return xs.reduce((c, x) => c.then(x), Promise.resolve());
}
const pipe = (first: any, ...rest: ((x: any) => any)[]) => {
return rest.reduce((x, f) => f(x), first);
}
const curry2 = <A extends any, B extends any, C extends any>
(f: (x: A, y: B) => C) => (x: A) => (y: B) => f(x, y);
const flip = <A extends any, B extends any, C extends any>
(f: (x: A) => ((y: B) => C)) => (y: B) => (x: A) => f(x)(y);
const _bind1 = flip(curry2(_bind));
const _withLeft1 = flip(curry2(_withLeft));
const mLog = (...args) => { console.log(...args); return _return(null); }
// Examples:
// =========
//
// First, left identity
//
async function testFirstMonadLaw() {
const f = (x: number) => _return(x + 1);
const a = 5;
console.log("Testing left identity (first monad law)");
await assertEq( pipe(_return(a), _bind1(f)) // return a >>= f
, f(a));
};
// Second, right identity
//
async function testSecondMonadLaw() {
const m = _return(42);
console.log("Testing right identity (second monad law)");
await assertEq( m
, pipe(m, _bind1(_return))); // m >>= return
};
// Third, associativity
//
async function testThirdMonadLaw() {
const m = _return(2.712);
const g = (x: number) => _return(x - 1);
const f = (x: number) => _return(x * 2);
console.log("Testing associativity (third monad law)");
await assertEq( pipe(pipe(m, _bind1(g)), _bind1(f)) // (m >>= g) >>= f
, _bind(m, (x) => _bind(g(x), f))); // m >>= \x -> g x >>= f
}
// OK, so those are all a bit contrived, and not obviously useful, but
// where might this view of Promises by helpful? Monads allow us to
// represent computations and the ergonomics of Either (such that bind
// will never execute a function on a Left value, effectively preventing
// a compuation propagating in an error state) is really comfortable.
async function errorStatePropagation() {
console.log("Demonstrating error state propagation");
const f = (a: number) =>
pipe( _return(a)
, _bind1((x: number) => _return(x * 2))
, _bind1((x: number) => x < 10 ? _right(x) : _left("too big"))
, _bind1((x: number) => _return("Got " + x))
, _withLeft1((e: string) => _return("Failed with " + e))
);
// This is the equivalent of:
// f a = either ((<>) "Failed with ") ((<>) "Got " . show) $
// return a
// >>= return . (* 2)
// >>= \x -> if x < 10 then Right x else Left "too big"
await assertEq(f(3), _return("Got 6"));
await assertEq(f(7), _return("Failed with too big"));
}
// Run tests:
//
console.log("=== TESTING HYPOTHESIS ===");
seq([
testFirstMonadLaw,
testSecondMonadLaw,
testThirdMonadLaw,
errorStatePropagation
]).then(() => console.log("=== ALL PASSED ==="));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment