Created
April 18, 2022 16:17
-
-
Save gfarrell/d2504d12f85e80a8cc2933d587d320b3 to your computer and use it in GitHub Desktop.
Can we treat Promises as Monads?
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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