-
-
Save getify/506882cb4926a6417d6dc7deff0f184c to your computer and use it in GitHub Desktop.
in JS, exploring async (promise-based) Haskell-style `do`-block syntax for the IO monad Demo: https://codepen.io/getify/pen/abvjRRK?editors=0011
This file contains hidden or 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
// setup all the utilities to be used | |
var delay = (ms,cancel = new AbortController()) => { | |
var pr = new Promise(res => { | |
var intv = setTimeout(res,ms); | |
cancel.signal.addEventListener("abort",() => { clearTimeout(intv); res(); }); | |
}); | |
pr.abort = () => cancel.abort(); | |
return pr; | |
}; | |
var log = msg => IO(() => console.log(msg)); | |
var wait = ms => IO(() => delay(ms)); | |
var getCountdown = (idx = 3) => () => IO(async () => { | |
await delay(500); return idx--; | |
}); | |
var bindEvent = (el,evtType,onEvt) => IO(() => { | |
el.addEventListener(evtType,onEvt,false); | |
}); | |
var unbindEvent = (el,evtType,onEvt) => IO(() => { | |
el.removeEventListener(evtType,onEvt,false); | |
}); | |
var onEscape = handler => evt => { | |
if (evt.key == "Escape") { | |
handler(); | |
} | |
}; | |
function reportError(err) { | |
if (typeof err._inspect == "function") { | |
console.error(err._inspect()); | |
} | |
else if (typeof err.toString == "function") { | |
console.error(err.toString()); | |
} | |
else { | |
console.error("An unknown error was caught!"); | |
} | |
} |
This file contains hidden or 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
// define two IO do-block tasks (`prepareCountdown*()` and `doCountdown*()`) | |
function *prepareCountdown(){ | |
var countdownCanceled = false; | |
var handler = onEscape(() => { | |
countdownCanceled = true; | |
wait3sec.abort(); | |
}); | |
yield log("preparing the countdown... (hit <esc> to cancel)"); | |
yield bindEvent(document,"keydown",handler); | |
var wait3sec; | |
yield IO(() => (wait3sec = wait(3000).run())); | |
yield unbindEvent(document,"keydown",handler); | |
if (countdownCanceled) { | |
throw "countdown canceled."; | |
} | |
return IO.do(doCountdown(/*startAt=*/5)); | |
} | |
function *doCountdown(startAt){ | |
var countdownStopped = false; | |
var handler = onEscape(() => { | |
countdownStopped = true; | |
}); | |
yield log("starting countdown... (hit <esc> to cancel)"); | |
yield bindEvent(document,"keydown",handler); | |
var tick = getCountdown(startAt); | |
while (true) { | |
let counter = yield tick(); | |
if (counter === 0 || countdownStopped) { | |
break; | |
} | |
yield log(counter); | |
} | |
yield unbindEvent(document,"keydown",handler); | |
if (countdownStopped) { | |
throw "countdown stopped."; | |
} | |
else { | |
yield log("countdown complete!"); | |
} | |
} |
This file contains hidden or 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
// how to run the demo | |
// HINT: click to focus the rendered page view to be able to use the keyboard event in this demo | |
IO.do(prepareCountdown) | |
.chain(nextTask => nextTask) | |
.run() | |
.catch(reportError); |
This file contains hidden or 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
var IO = (function DefineIO() { | |
const brand = {}; | |
return Object.assign(IO,{ of, is, do: $do, doEither, }); | |
// ************************** | |
function IO(effect) { | |
var publicAPI = { | |
map, chain, flatMap: chain, bind: chain, | |
ap, run, _inspect, _is, | |
[Symbol.toStringTag]: "IO", | |
}; | |
return publicAPI; | |
// ********************* | |
function map(fn) { | |
return IO(v => { | |
var res = effect(v); | |
return ( | |
_isPromise(res) ? | |
res.then(fn) : | |
fn(res) | |
); | |
}); | |
} | |
function chain(fn) { | |
return IO(v => { | |
var res = effect(v); | |
return ( | |
_isPromise(res) ? | |
res.then(fn).then(v2 => v2.run(v)) : | |
fn(res).run(v) | |
); | |
}); | |
} | |
function ap(m) { | |
return m.map(effect); | |
} | |
function run(v) { | |
return effect(v); | |
} | |
function _inspect() { | |
return `${publicAPI[Symbol.toStringTag]}(${ | |
typeof effect == "function" ? (effect.name || "anonymous function") : | |
(effect && typeof effect._inspect == "function") ? effect._inspect() : | |
val | |
})`; | |
} | |
function _is(br) { | |
return br === brand; | |
} | |
} | |
function of(v) { | |
return IO(() => v); | |
} | |
function is(v) { | |
return v && typeof v._is == "function" && v._is(brand); | |
} | |
function processNext(next,respVal,outerV) { | |
return (new Promise(async (resv,rej) => { | |
try { | |
await monadFlatMap( | |
(_isPromise(respVal) ? await respVal : respVal), | |
v => IO(() => next(v).then(resv,rej)) | |
).run(outerV); | |
} | |
catch (err) { | |
rej(err); | |
} | |
})); | |
} | |
function $do(block) { | |
return IO(outerV => { | |
var it = getIterator(block,outerV); | |
return (async function next(v){ | |
var resp = it.next(_isPromise(v) ? await v : v); | |
resp = _isPromise(resp) ? await resp : resp; | |
return ( | |
resp.done ? | |
resp.value : | |
processNext(next,resp.value,outerV) | |
); | |
})(); | |
}); | |
} | |
function doEither(block) { | |
return IO(outerV => { | |
var it = getIterator(block,outerV); | |
return (async function next(v){ | |
try { | |
v = _isPromise(v) ? await v : v; | |
let resp = ( | |
Either.Left.is(v) ? | |
it.throw(v) : | |
it.next(v) | |
); | |
resp = _isPromise(resp) ? await resp : resp; | |
let respVal = ( | |
resp.done ? | |
( | |
(_isPromise(resp.value) ? await resp.value : resp.value) | |
) : | |
resp.value | |
); | |
return ( | |
resp.done ? | |
( | |
Either.Right.is(respVal) ? | |
respVal : | |
Either.Right(respVal) | |
) : | |
processNext(next,respVal,outerV) | |
.catch(next) | |
); | |
} | |
catch (err) { | |
throw ( | |
Either.Left.is(err) ? | |
err : | |
Either.Left(err) | |
); | |
} | |
})(); | |
}); | |
} | |
function getIterator(block,v) { | |
return ( | |
typeof block == "function" ? block(v) : | |
(block && typeof block == "object" && typeof block.next == "function") ? block : | |
undefined | |
); | |
} | |
function monadFlatMap(m,fn) { | |
return m[ | |
"flatMap" in m ? "flatMap" : | |
"chain" in m ? "chain" : | |
"bind" | |
](fn); | |
} | |
function _isPromise(v) { | |
return v && typeof v.then == "function"; | |
} | |
})(); | |
var Either = (function DefineEither() { | |
const brand = {}; | |
Left.is = LeftIs; | |
Right.is = RightIs; | |
return Object.assign(Either,{ | |
Left, Right, of: Right, pure: Right, | |
unit: Right, is, fromFoldable, | |
}); | |
// ************************** | |
function Left(val) { | |
return LeftOrRight(val,/*isRight=*/ false); | |
} | |
function LeftIs(val) { | |
return is(val) && !val._is_right(); | |
} | |
function Right(val) { | |
return LeftOrRight(val,/*isRight=*/ true); | |
} | |
function RightIs(val) { | |
return is(val) && val._is_right(); | |
} | |
function Either(val) { | |
return LeftOrRight(val,/*isRight=*/ true); | |
} | |
function LeftOrRight(val,isRight = true) { | |
var publicAPI = { | |
map, chain, flatMap: chain, bind: chain, | |
ap, fold, _inspect, _is, _is_right, | |
get [Symbol.toStringTag]() { | |
return `Either:${isRight ? "Right" : "Left"}`; | |
}, | |
}; | |
return publicAPI; | |
// ********************* | |
function map(fn) { | |
return ( | |
isRight ? | |
LeftOrRight(fn(val),isRight) : | |
publicAPI | |
); | |
} | |
function chain(fn) { | |
return ( | |
isRight ? | |
fn(val) : | |
publicAPI | |
); | |
} | |
function ap(m) { | |
return m.map(val); | |
} | |
function fold(asLeft,asRight) { | |
return ( | |
isRight ? | |
asRight(val) : | |
asLeft(val) | |
); | |
} | |
function _inspect() { | |
return `${publicAPI[Symbol.toStringTag]}(${ | |
typeof val == "string" ? JSON.stringify(val) : | |
typeof val == "undefined" ? "" : | |
typeof val == "function" ? (val.name || "anonymous function") : | |
val && typeof val._inspect == "function" ? val._inspect() : | |
val | |
})`; | |
} | |
function _is(br) { | |
return br === brand; | |
} | |
function _is_right() { | |
return isRight; | |
} | |
} | |
function is(val) { | |
return val && typeof val._is == "function" && val._is(brand); | |
} | |
function fromFoldable(m) { | |
return m.fold(Left,Right); | |
} | |
})(); |
This file contains hidden or 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
// proofs for this IO monad | |
var f = v => IO.of(v); | |
// (1) left identity | |
console.log( | |
IO.of(42).chain(f).run() === f(42).run() | |
); // true | |
// (2) right identity | |
console.log( | |
IO.of(42).chain(IO.of).run() === IO.of(42).run() | |
); // true | |
// (3) associativity | |
console.log( | |
IO.of(3).chain(v => IO.of(v * 7)).chain(v => IO.of(v * 2)).run() === | |
IO.of(3).chain(v => IO.of(v * 7).chain(v => IO.of(v * 2))).run() | |
); // true | |
// and furthermore... | |
IO(v => `The value of v is: ${v}`) | |
.ap( | |
IO.of(3) | |
.map(v => v * 2) | |
.chain(v => IO.of(v * 7)) | |
) | |
.run(); | |
// The value of v is: 42 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment