Is there a better way to mimic do notation in JS?

bob :

Monadic computations quickly become confusing in JS:

const chain = fm => xs =>
  xs.reduce((acc, x) => acc.concat(fm(x)), []);

const of = x => [x];

const main = xs => ys => zs =>
  chain(x =>
    x === 0
      ? []
      : chain(y =>
          chain(z => [[x, y, z]]) (zs)) (ys)) (xs);

console.log("run to completion",
  main([1, 2]) (["a", "b"]) ([true, false]));
  
console.log("short circuiting",
  main([0, 2]) (["a", "b"]) ([true, false]));

In Haskell do notation can be used to hide the nested function calls. However, do is a compile time technique that Javascript lacks.

Generator functions seem to be a good fit, but they don't work with monads that supply a priorized choice. So I have been looking for an alternative for quite some time and recently came up with a sort of monadic applicator in order to untangle the nested computation a bit:

const chain = fm => xs =>
  xs.reduce((acc, x) => acc.concat(fm(x)), []);

const of = x => [x];

const id = x => x;

const infixM3 = (w, f, x, g, y, h, z) =>
  f(x_ =>
    w(x_, w_ => g(y_ =>
      w_(y_, w__ => h(z_ =>
        w__(z_, id)) (z))) (y))) (x);

const mainApp = xs => ys => zs => infixM3(
  (x, k) =>
    x === 0
      ? []
      : k((y, k) =>
          k((z, k) => [[x, y, z]])),
  chain, xs,
  chain, ys,
  chain, zs);

console.log("run to completion",
  mainApp([1, 2]) (["a", "b"]) ([true, false]));

console.log("short circuiting",
  mainApp([0, 2]) (["a", "b"]) ([true, false]));

The applicator mimics chain in infix position hence the name. It is based on local continuations, therefore the lifted function expects a pair of arguments constisting of the bound value and the continuation respectively. If the continuation isn't applied the computation short circuits. It looks complicated but is rather a mechanical process.

Comparing the explicit version with the abstracted one I think this is an improvement in terms of readability:

chain(x =>
  x === 0
    ? []
    : chain(y =>
        chain(z => [[x, y, z]]) (zs)) (ys)) (xs);

infixM3(
  (x, k) =>
    x === 0
      ? []
      : k((y, k) =>
          k((z, k) => [[x, y, z]])),
  chain, xs,
  chain, ys,
  chain, zs);

The continuations in the lifted function bother though as well as the fact that the applicator is arity aware. Besides it doesn't look like do-notation at all. Can we get closer to a syntax that resembles do notation?

Aadit M Shah :

You can use the immutagen library to write monadic code using generators.

const monad = bind => regen => (...args) => function loop({ value, next }) {
    return next ? bind(value, val => loop(next(val))) : value;
}(immutagen.default(regen)(...args));

const flatMap = (array, callback) => array.flatMap(callback);

const list = monad(flatMap);

const main = list(function* (xs, ys, zs) {
    const x = yield xs;
    if (x === 0) return [];
    const y = yield ys;
    const z = yield zs;
    return [[x, y, z]];
});

console.log("run to completion", main([1, 2], ["a", "b"], [true, false]));

console.log("short circuiting", main([0, 2], ["a", "b"], [true, false]));
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://unpkg.com/[email protected]/immutagen.js"></script>

As you can see, this works with non-deterministic monads too. However, it has two disadvantages.

  1. It's inefficient because multiple generators need to be created and replayed, leading to quadratic growth in time complexity.
  2. It only works for pure monads and pure computations because multiple generators need to be created and replayed. Hence, side effects would be incorrectly executed multiple times.

Nevertheless, even if you don't use generators writing monadic code in JavaScript isn't so bad.

const flatMap = (array, callback) => array.flatMap(callback);

const main = (xs, ys, zs) =>
    flatMap(xs, x =>
    x === 0 ? [] :
    flatMap(ys, y =>
    flatMap(zs, z =>
    [[x, y, z]])));

console.log("run to completion", main([1, 2], ["a", "b"], [true, false]));

console.log("short circuiting", main([0, 2], ["a", "b"], [true, false]));
.as-console-wrapper { max-height: 100% !important; top: 0; }

At the end of the day, if you want the best of both worlds then you'll either need to use a language that compiles to JavaScript or use a preprocessor like Babel or sweet.js.

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=7207&siteId=1