C'è un modo migliore per imitare la notazione in JS?

2020-02-14 javascript functional-programming monads do-notation

I calcoli monadici diventano rapidamente confusi 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 notazione può essere utilizzato per nascondere le chiamate di funzione nidificate. Tuttavia, do è una tecnica in fase di compilazione che manca di Javascript.

Le funzioni del generatore sembrano adattarsi bene, ma non funzionano con le monadi che forniscono una scelta prioritaria. Quindi ho cercato un'alternativa per un po 'di tempo e recentemente ho trovato una sorta di applicatore monadico per districare un po' il calcolo nidificato:

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]));

L'applicatore imita la catena nella posizione di infissione, quindi il nome. Si basa su continuazioni locali, pertanto la funzione sollevata prevede una coppia di argomenti che consistono rispettivamente nel valore associato e nella continuazione. Se la continuazione non viene applicata, i cortocircuiti di calcolo. Sembra complicato ma è piuttosto un processo meccanico.

Confrontando la versione esplicita con quella astratta penso che questo sia un miglioramento in termini di leggibilità:

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);

Le continuazioni nella funzione sollevata disturbano però, così come il fatto che l'applicatore è consapevole dell'arity. Inoltre non sembra affatto una notazione. Possiamo avvicinarci a una sintassi che ricorda do la notazione?

Answers

Puoi usare la libreria immutagen per scrivere codice monadico usando i generatori.

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>

Come puoi vedere, funziona anche con monadi non deterministiche. Tuttavia, presenta due svantaggi.

  1. È inefficiente perché è necessario creare e riprodurre più generatori, portando a una crescita quadratica della complessità temporale.
  2. Funziona solo per monadi pure e calcoli puri perché è necessario creare e riprodurre più generatori. Pertanto, gli effetti collaterali verrebbero eseguiti in modo errato più volte.

Tuttavia, anche se non usi generatori che scrivono codice monadico in JavaScript non è poi così male.

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; }

Alla fine della giornata, se vuoi il meglio di entrambi i mondi, dovrai usare un linguaggio che si compili in JavaScript o usare un preprocessore come Babel o sweet.js .

Related