DEV Community

Iven Marquardt
Iven Marquardt

Posted on

Another Flaw of the Promise Type: Intertwining of in-sequence and in-parallel

From a functional perspective Promise is a poorly designed data type, because it is lawless, an unprincipled abstraction, rather belonging to the quirky part of Javascript.

In this brief post I will demonstrate another flaw of the Promise type: It intermingles asynchronous computations that have an in-sequence semantics with those that have an in-parallel one.

Why should both forms be distinguished? Because...

  • async computations in-parallel are not monads (in-sequence are)
  • both result in different algebraic structures

The former statement is clear, provided you know what a monad is. However, the ladder is a bit harder. Both forms of async computations are just very different and thus their approaches to handle different scenarios vary. Let's compare their monoids to illustrate this statement.

Task - async in sequence

Task sequentially performs asynchronous computations. It is a monad but also a monoid:

// Task type

const Task = task => record(
  thisify(o => {
    o.task = (res, rej) =>
      task(x => {
        o.task = k => k(x);
        return res(x);
      }, rej);

    return o;

// Task monoid

const tEmpty = empty =>
  () => Task((res, rej) => res(empty()));

const tAppend = append => tx => ty =>
  Task((res, rej) =>
    tx.task(x =>
      ty.task(y =>
        res(append(x) (y)), rej), rej));

// Number monoid under addition

const sumAppend = x => y => x + y;
const sumEmpty = () => 0;

// some async functions

const delayTask = f => ms => x =>
  Task((res, rej) => setTimeout(comp(res) (f), ms, x));

const tInc = delayTask(x => x + 1) (10); // 10ms delay
const tSqr = delayTask(x => x * x) (100); // 100ms delay


const main = tAppend(sumAppend) (tSqr(5)) (tInc(5));
//                   ^^^^^^^^^ monoid of the base type

main.task(console.log); // logs 31

run code

Do you see how succinct this Task implementation is compared to a Promise/A+ compliant one?

The monoid takes a monoid from a base type and lifts it into the context of asynchronous computations in sequence, that is, tAppend takes a monoid from another type and applies it as soon as both async operations have yielded a result. Don't worry if this is too abstract. We will have an example soon.

Parallel - async in parallel

Parallel performa asynchronous computations in parallel. It is only an applicative and monoid but not a monad:

// Parallel type

const Parallel = para => record(
  thisify(o => {
    o.para = (res, rej) =>
      para(x => {
        o.para = k => k(x);
        return res(x);
      }, rej);

    return o;

// Parallel monoid

const pEmpty = () => Parallel((res, rej) => null);

const pAppend = tx => ty => {
  const guard = (res, rej) => [
    x => (
      isRes || isRej
        ? false
        : (isRes = true, res(x))),
    e =>
        isRes || isRej
          ? false
          : (isRej = true, rej(e))];

  let isRes = false,
    isRej = false;

  return Parallel(
    (res, rej) => {
      tx.para(...guard(res, rej));
      ty.para(...guard(res, rej))

// some async functions

const delayPara = f => ms => x =>
  Parallel((res, rej) => setTimeout(comp(res) (f), ms, x));

const pInc = delayPara(x => x + 1) (10); // 10ms delay
const pSqr = delayPara(x => x * x) (100); // 100ms delay


const main = pAppend(pSqr(5)) (pInc(5));

main.para(console.log); // logs 6

run code

Parallel's monoid instance represents the race monoid, i.e. pAppend picks the result value of the faster one of two asynchronous computations.


Both monoids are completely different, because Task and Parallel are different notions of asynchronous computations. Separating them is laborious at first but leads to more declarative, more predictable and more reliable code. There is a transformation between Task and Parallel and vice versa, so you can easily switch between both representations.

Read more on functional programming in Javascript in my online course.

Top comments (0)