Folding Promises in JavaScript

Folding Promises in JavaScript

Functional programming is all about transformations. For the actual transformations we use things called transformers. Transformers are typically implemented as pure functions that takes an input and produces a result. There is a formal name for the transformation process itself: morphism. Don't worry, this article is not about Xenomorphs from popular Alien movie ;] Now, let's see some morphisms in action.

Endomorphism

Endomorphism is a transformation that satisfies the criteria: input and output of the transformer must be from the same category.

// add1 :: Number -> Number
const add1 = val => val + 1;

Isomorphism

Isomorphism is a pair of transformations between two categories with no data loss.

// objToArray :: {a: Number} -> SingleItemArray
//     SingleItemArray = Array.<Number>
const objToArray = ({ a }) => [a];

// arrayToObj :: SingleItemArray -> {a: Number}
//     SingleItemArray = Array.<Number>
const arrayToObj = ([a]) => ({a});

Homomorphism

Homomorphism is a structure preserving transformation. We always stay in the same category. What does it even mean ? Well any functor is by definition a homorphism. I bet you are already using it. Mapping a JavaScript Array is one example. When you map an array, you transform item by item and the result is again an array.

const a = [1, 2, 3]; // => Array[1, 2, 3]
const b = a.map(add1); // => Array[2, 3, 4]

What we have done here is endomorphic homomorphism. We transformed the array into another array using endomorphic function add1.

Catamorphism

Catamorphism is a way folding a type into a value. The transformation is done from one category into another one. It is usually not possible to transform the value back to type because the structure of the type is lost during the transformation.

const type = [1, 2, 3];
const value = type.reduce((sum, item) => sum + item, 0); // Number(6)

Now that we've got our theory right , let's deep into the original subject of this article. Folding a promise or list of promises (serially) into a value. Well not specifically a value, but the promise that holds the value inside it. It is the nature of the promises that if you enter their realm you have to stay in their realm. So basically what we will be doing here can be called catamorphic homomorphism.

Lets start by folding a list of promises sequentially into accumulator.

const listP = [Promise.resolve(1), Promise.resolve(2)];
const identityValue = Promise.resolve(0);


const result = listP.reduce((acc, promise) => {
  return acc.then(sum => promise.then(innerVal => sum + innerVal));
}, identityValue); // => Promise(3)

Quite simple and straight forward. But there are a couple of problems there. We also leak some implementation details out. We also learned few things. Our identity value must always be Promise because we depend on it in our iterator function. Our iterator function must always return promise for reduce to work in next iteration. How can we make it better ? Let's start by removing the requirement for identity value to always be the promise.

const listP = [Promise.resolve(1), Promise.resolve(2)];
const identityValue = 0;

const reduceP = (fn, identityValue, list) => {
  const identityValueP = Promise.resolve(identityValue);

  return list.reduce(fn, identityValueP);
}

const result = reduceP((acc, promise) => {
  return acc.then(sum => promise.then(innerVal => sum + innerVal));
}, identityValue, listP); //=> Promise(3)

Better. But our iterator function must still return the promise. Let's write a version where our iterator function may or may not return a promise.

const add = (a, b) => a + b;
const listP = [Promise.resolve(1), Promise.resolve(2)];
const identityValue = 0;

const reduceP = (fn, identityValue, list) => {
  const identityValueP = Promise.resolve(identityValue);

  return list.reduce((acc, promise) => acc
    .then(sum => Promise.all([sum, promise]))
    .then(([sum, value]) => fn(sum, value))
  , identityValueP);
}

const result = reduceP(add, identityValue, listP); //=> Promise(3)

Perfect. What we've done here is lifting the add function into promise realm/context and the function arguments are applied as synchronous values. But we can still generate new promises from our iteration function. We can go even further and remove the requirement of the listP to contain only promises, or even make the listP promise that returns list of values or promises. We can even apply some practical additional rules, but I won't go into details of these.

Doing my research into this, it turned out that there is already a library that can fold a promises in a sequential manner. It's called Bluebird and it contains a reduce method that does exactly that. Never the less if you are a purist like me and want to stick with the native promises and functional libraries like ramda, don't worry. I implemented the reduceP and reduceRightP function into ramda-adjunct to make our life easier and implemented the same rules as Bluebirds reduce implements and also added some other nifty features on top of it. Check it out and let me know what you think.

That concludes our business for today. Again I end my article with the usual axiom: Define your function as pure morphisms and lift them into different context if and when needed. And compose, compose, compose...

Article was also published on codementor.io.

Your arrayToObj example doesn't work (as of node 8.2.1). The parser interprets the curly braces as a function block instead of a shorthand object to be returned. ([a]) => { return {a}; } works though! I like the theory you present in this example. I've been using vanilla Promise.all and then passing in a mapping function, but this explanation of the design rationale is nice.

Like
Reply

To view or add a comment, sign in

Insights from the community

Explore topics