Skip to content

Commit

Permalink
IO/IOx: improved error handling and IO's trampolinging (per getify#9)…
Browse files Browse the repository at this point in the history
…, improved tests and test coverage
  • Loading branch information
getify committed Jan 13, 2022
1 parent 52dab96 commit 6164480
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 89 deletions.
96 changes: 26 additions & 70 deletions src/io/io.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ module.exports._IS_CONT = IS_CONT;
// **************************

function IO(effect) {
const TAG = "IO";
var publicAPI = {
map, chain, flatMap: chain, bind: chain,
concat, run, _inspect, _is,
[Symbol.toStringTag]: "IO",
[Symbol.toStringTag]: TAG,
};
return publicAPI;

Expand Down Expand Up @@ -192,10 +193,9 @@ function $do($V,...args) {
return IO(outerEnv => {
var it = getIterator($V,outerEnv,/*outerThis=*/this,args);

return trampoline(
next(),
err => trampoline(next(err,"error"),liftDoError)
);
return (new Promise(res => res(trampoline(next()))))
.catch(err => trampoline(next(err,"error")))
.catch(liftDoError);

// ************************************************

Expand All @@ -214,10 +214,7 @@ function $do($V,...args) {
// trampoline()s here unwrap the continuations
// immediately, because we're already in an
// async microtask from the promise
resp.then(
v => trampoline(handleResp(v)),
err => trampoline(handleError(err))
) :
resp.then(v => trampoline(handleResp(v))) :

handleResp(resp)
);
Expand Down Expand Up @@ -251,18 +248,6 @@ function $do($V,...args) {
return processNext(next,resp.value,outerEnv,/*throwEither=*/false);
}
}

function handleError(err) {
// already tried to throw the error in?
if (type == "error") {
return liftDoError(err);
}
// otherwise, at least try to throw
// the error back in
else {
return next(err,"error");
}
}
}
catch (err) {
return liftDoError(err);
Expand All @@ -282,10 +267,9 @@ function doEither($V,...args) {
return IO(outerEnv => {
var it = getIterator($V,outerEnv,/*outerThis=*/this,args);

return trampoline(
next(),
err => trampoline(next(err,"error"),liftDoEitherError)
);
return (new Promise(res => res(trampoline(next()))))
.catch(err => trampoline(next(err,"error")))
.catch(liftDoEitherError);

// ************************************************

Expand Down Expand Up @@ -314,10 +298,7 @@ function doEither($V,...args) {
// trampoline()s here unwrap the continuations
// immediately, because we're already in an
// async microtask from the promise
resp.then(
v => trampoline(handleResp(v)),
err => trampoline(handleError(err))
) :
resp.then(v => trampoline(handleResp(v))) :

handleResp(resp)
);
Expand Down Expand Up @@ -372,18 +353,6 @@ function doEither($V,...args) {
return Either.Right(respVal);
}
}

function handleError(err) {
// already tried to throw the error in?
if (type == "error") {
return liftDoEitherError(err);
}
// otherwise, at least try to throw
// the error back in
else {
return next(err,"error");
}
}
}
catch (err) {
return liftDoEitherError(err);
Expand Down Expand Up @@ -457,28 +426,21 @@ function returnRunContinuation(env) {
// only used internally, prevents RangeError
// call-stack overflow when composing many
// IOs together
function trampoline(res,onUnhandled = (err) => { throw err; }) {
function trampoline(res) {
var stack = [];

processContinuation: while (Array.isArray(res) && res[IS_CONT] === true) {
let left = res[0];
let leftRes;

try {
// compute the left-half of the continuation
// tuple
leftRes = left();

// store left-half result directly in the
// continuation tuple (for later recall
// during processing right-half of tuple)
// res[0] = { [CONT_VAL]: leftRes };
res[0] = leftRes;
}
catch (err) {
res = onUnhandled(err);
continue processContinuation;
}

// compute the left-half of the continuation
// tuple
let leftRes = left();

// store left-half result directly in the
// continuation tuple (for later recall
// during processing right-half of tuple)
// res[0] = { [CONT_VAL]: leftRes };
res[0] = leftRes;

// store the modified continuation tuple
// on the stack
Expand All @@ -501,18 +463,12 @@ function trampoline(res,onUnhandled = (err) => { throw err; }) {
while (stack.length > 0) {
let [ , right ] = stack.pop();

try {
res = right(res);
res = right(res);

// right half of continuation tuple returned
// another continuation?
if (Array.isArray(res) && res[IS_CONT] === true) {
// process the next continuation
continue processContinuation;
}
}
catch (err) {
res = onUnhandled(err);
// right half of continuation tuple returned
// another continuation?
if (Array.isArray(res) && res[IS_CONT] === true) {
// process the next continuation
continue processContinuation;
}
}
Expand Down
45 changes: 34 additions & 11 deletions src/io/iox.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ var {
curry,
getDeferred,
} = require("../lib/util.js");
var Either = require("../either.js");
var IO = require("./io.js");

// curry some public methods
Expand Down Expand Up @@ -51,6 +52,7 @@ function IOx(iof,deps = []) {
deps = [ deps ];
}

const TAG = "IOx";
var currentEnv = UNSET;
var currentVal = UNSET;
var waitForDeps;
Expand All @@ -73,7 +75,7 @@ function IOx(iof,deps = []) {
map, chain, flatMap: chain, bind: chain,
concat, run, stop, close, isClosed, freeze,
isFrozen, toString, _chain_with_IO, _inspect,
_is, [Symbol.toStringTag]: "IOx",
_is, [Symbol.toStringTag]: TAG,
});
registerHooks.set(publicAPI,[ registerListener, unregisterListener, ]);
return publicAPI;
Expand Down Expand Up @@ -708,17 +710,23 @@ function IOx(iof,deps = []) {
return (
(currentVal !== UNSET && currentEnv !== UNSET) ?
fn(currentVal !== CLOSED ? currentVal : undefined) :
io.chain(fn)
io ? io.chain(fn) :
fn(undefined)
);
}

function _inspect() {
return `${publicAPI[Symbol.toStringTag]}(${
isMonad(currentVal) && isFunction(currentVal._inspect) ? currentVal._inspect() :
![ UNSET, CLOSED ].includes(currentVal) ? String(currentVal) :
isFunction(iof) ? (iof.name || "anonymous function") :
".."
})`;
if (closing) {
return `${TAG}(-closed-)`;
}
else {
return `${publicAPI[Symbol.toStringTag]}(${
isMonad(currentVal) && isFunction(currentVal._inspect) ? currentVal._inspect() :
![ UNSET, CLOSED ].includes(currentVal) ? String(currentVal) :
isFunction(iof) ? (iof.name || "anonymous function") :
".."
})`;
}
}

function _is(br) {
Expand Down Expand Up @@ -775,6 +783,7 @@ function onEvent(el,evtName,evtOpts = false) {
subscribed = true;

// (lazily) setup event listener
/* istanbul ignore next */
if (isFunction(el.addEventListener)) {
el.addEventListener(evtName,iox,evtOpts);
}
Expand All @@ -792,6 +801,7 @@ function onEvent(el,evtName,evtOpts = false) {
subscribed = false;

// remove event listener
/* istanbul ignore next */
if (isFunction(el.removeEventListener)) {
el.removeEventListener(evtName,iox,evtOpts);
}
Expand Down Expand Up @@ -1303,6 +1313,7 @@ function fromIter($V,closeOnComplete = true) {
}
else {
// note: should never get here
/* istanbul ignore next */
break;
}
}
Expand Down Expand Up @@ -1521,8 +1532,20 @@ function safeIORun(io,env) {
}
}

/* istanbul ignore next */
function logUnhandledError(err) {
console.log(err && (
err.stack ? err.stack : err.toString()
));
if (Either.Left.is(err)) {
console.log(
err.fold(v=>v,()=>{})
);
}
else if (isMonad(err)) {
console.log(err._inspect());
}
else if (isFunction(err.toString)) {
console.log(err.toString());
}
else {
console.log(err);
}
}
1 change: 0 additions & 1 deletion tests/io-helpers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ qunit.test("doEIOBind", async (assert) => {
);
});


qunit.test("iif", async (assert) => {
var condEq = v => IO(env => v === env);

Expand Down
2 changes: 1 addition & 1 deletion tests/io.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -799,7 +799,7 @@ qunit.test("IO.doEither", async (assert) => {
}

assert.ok(
r9 == "seven 1",
Either.Left.is(r9) && r9._inspect() == "Either:Left(\"seven 1\")",
"do-either routine returns promise rejection from yielded IO holding rejected promise"
);
});
Expand Down
58 changes: 52 additions & 6 deletions tests/iox.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1293,8 +1293,22 @@ qunit.test("IOx.do/doEither", async (assert) =>{
return Either.Right(v * 5);
}

function *three(env,v) {
var a = IOx.of(env);
var b = IOx.of(v);
b.close();

res3.push( b._inspect() );
res3.push( a.run() );
res3.push( yield a );
res3.push( yield b );

return (env + v);
}

var res1 = [];
var res2 = [];
var res3 = [];
var i1 = IOx.do(one,[ 6 ],7);
var i2 = IOx.doEither(two,[ i1 ]);
var i3 = IOx((env,v) => {
Expand All @@ -1304,18 +1318,20 @@ qunit.test("IOx.do/doEither", async (assert) =>{
v2 => v2 * 2
);
},[ i2 ]);
var i4 = IOx.do(three,[ 20 ]);

var res3 = i3.run(4);
var res4 = i3.run(4);
var res5 = i4.run(10);

assert.ok(
res3 instanceof Promise,
res4 instanceof Promise,
"IOx with IO.doEither dep produces a promise"
);

res3 = await res3;
res4 = await res4;

assert.equal(
res3,
res4,
190,
"IOx do/doEither values flow through eventually"
);
Expand Down Expand Up @@ -1354,10 +1370,10 @@ qunit.test("IOx.do/doEither", async (assert) =>{
"IOx.doEither re-evaluated when dependency IOx.do is manually updated"
);

res3 = i3.run(4);
res4 = i3.run(4);

assert.equal(
res3,
res4,
250,
"IOx final value eventually resolves"
);
Expand All @@ -1367,4 +1383,34 @@ qunit.test("IOx.do/doEither", async (assert) =>{
[ "two", 4, 19, 2, "Either:Right(95)", "two", 4, 25, 2, "Either:Right(125)", "Either:Right(125)" ],
"re-running IOx manually does not re-run dependency IOx.doEither"
);

res5 = await res5;

assert.equal(
res5,
30,
"(1) IOx.do() routine eventually produces a return value"
);

assert.deepEqual(
res3,
[ "IOx(-closed-)", 10, 10, undefined ],
"(1) IOx.do() properly handles yields of already-run and already-closed IOxs"
);

res5 = await i4.run(100);

await delayPr(10);

assert.equal(
res5,
120,
"(2) IOx.do() routine eventually produces a return value"
);

assert.deepEqual(
res3,
[ "IOx(-closed-)", 10, 10, undefined, "IOx(-closed-)", 100, 100, undefined ],
"(2) IOx.do() properly handles yields of already-run and already-closed IOxs"
);
});

0 comments on commit 6164480

Please sign in to comment.