Skip to content

Instantly share code, notes, and snippets.

@getify
Last active August 18, 2020 06:22
Show Gist options
  • Save getify/506882cb4926a6417d6dc7deff0f184c to your computer and use it in GitHub Desktop.
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
// 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!");
}
}
// 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!");
}
}
// 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);
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);
}
})();
// 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