4

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?

  • 1
    I suppose you could use a preprocessor like Babel to add language features that you'd like to see; for example [this do expression extension](https://babeljs.io/docs/en/babel-plugin-proposal-do-expressions). – Luke Briggs Feb 14 '20 at 12:52
  • 1
    ClojureScript? You can do macros and it will compile down to JS – customcommander Feb 14 '20 at 19:07
  • Not with native JS, but there are many tools that make it easy to use your own syntax - for example https://www.sweetjs.org/ – Bergi Feb 14 '20 at 21:33
  • Related: https://stackoverflow.com/q/20729050/783743 – Aadit M Shah Feb 15 '20 at 03:46

1 Answers1

3

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/immutagen@1.0.9/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.

Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299