I have a variadic lifting function that allows for flat monadic chains without deeply nested function composition:
const varArgs = f => {
const go = args =>
Object.defineProperties(
arg => go(args.concat(arg)), {
"runVarArgs": {get: function() {return f(args)}, enumerable: true},
[TYPE]: {value: "VarArgs", enumerable: true}
});
return go([]);
};
const varLiftM = (chain, of) => f => { // TODO: replace recursion with a fold
const go = (ms, g, i) =>
i === ms.length
? of(g)
: chain(ms[i]) (x => go(ms, g(x), i + 1));
return varArgs(ms => go(ms, f, 0));
};
It works but I'd like to abstract from the recursion with a fold. A normal fold doesn't seem to work, at least not along with the Task
type,
const varLiftM = (chain, of) => f =>
varArgs(ms => of(arrFold(g => mx => chain(mx) (g)) (f) (ms))); // A
because the algebra in line A
would return a Task
for each iteration, not a partially applied function.
How can I replace the non-tail recursion with a fold?
Here is a working example of the current recursive implementation:
const TYPE = Symbol.toStringTag;
const struct = type => cons => {
const f = x => ({
["run" + type]: x,
[TYPE]: type,
});
return cons(f);
};
// variadic argument transformer
const varArgs = f => {
const go = args =>
Object.defineProperties(
arg => go(args.concat(arg)), {
"runVarArgs": {get: function() {return f(args)}, enumerable: true},
[TYPE]: {value: "VarArgs", enumerable: true}
});
return go([]);
};
// variadic monadic lifting function
const varLiftM = (chain, of) => f => { // TODO: replace recursion with a fold
const go = (ms, g, i) =>
i === ms.length
? of(g)
: chain(ms[i]) (x => go(ms, g(x), i + 1));
return varArgs(ms => go(ms, f, 0));
};
// asynchronous Task
const Task = struct("Task") (Task => k => Task((res, rej) => k(res, rej)));
const tOf = x => Task((res, rej) => res(x));
const tMap = f => tx =>
Task((res, rej) => tx.runTask(x => res(f(x)), rej));
const tChain = mx => fm =>
Task((res, rej) => mx.runTask(x => fm(x).runTask(res, rej), rej));
// mock function
const delay = (ms, x) =>
Task(r => setTimeout(r, ms, x));
// test data
const tw = delay(100, 1),
tx = delay(200, 2),
ty = delay(300, 3),
tz = delay(400, 4);
// specialization through partial application
const varAsyncSum =
varLiftM(tChain, tOf) (w => x => y => z => w + x + y + z);
// MAIN
varAsyncSum(tw) (tx) (ty) (tz)
.runVarArgs
.runTask(console.log, console.error);
console.log("1 sec later...");
[EDIT] As desired in the comments my fold implementation:
const arrFold = alg => zero => xs => {
let acc = zero;
for (let i = 0; i < xs.length; i++)
acc = alg(acc) (xs[i], i);
return acc;
};
That of
call around arrFold
seems a bit out of place.
I'm not sure whether your arrFold
is a right fold or left fold, but assuming it is a right fold you will need to use continuation passing style with closures just as you did in your recursive implementation:
varArgs(ms => of(arrFold(g => mx => chain(mx) (g)) (f) (ms)))
becomes
varArgs(ms => arrFold(go => mx => g => chain(mx) (x => go(g(x)))) (of) (ms) (f))
With a left fold, you could write
varArgs(arrFold(mg => mx => chain(g => map(g) (mx)) (mg)) (of(f)))
but you need to notice that this builds a different call tree than the right fold:
of(f)
chain(of(f))(g0 => map(m0)(g0))
chain(chain(of(f))(g0 => map(m0)(g0)))(g1 => map(m1)(g1))
chain(chain(chain(of(f))(g0 => map(m0)(g0)))(g1 => map(m1)(g1)))(g2 => map(m2)(g2))
vs (with the continuations already applied)
of(f)
chain(m0)(x0 => of(f(x0)))
chain(m0)(x0 => chain(m1)(x1 => of(f(x0)(x1))))
chain(m0)(x0 => chain(m1)(x1 => chain(m2)(x2) => of(f(x0)(x1)(x2)))))
According to the monad laws, they should evaluate to the same, but in practice one might be more efficient than the other.