Folding Promises in JavaScript

Published Aug 17, 2017Last updated Sep 29, 2017
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...

Original article publised on linkedin pulse.

Discover and read more posts from Vladimir Gorej
get started
Enjoy this post?

Leave a like and comment for Vladimir

14
6
6Replies
Bug Me Not
a month ago

Are you confusing categories and objects in the beginning?..

Vladimir Gorej
a month ago

Nope I am not. In type inference comments all category names must be capitalized. Let’s collaborate on the following comment and express what I had in mind when creating it:

// add1 :: Number -> Number

  • add1 is endomorhism
  • add1 is a total function

Number in this case is not an JavaScript type but an abstract category which includes all Float and Integer numbers. If you provide NaN, -Infinity, +Infinity then the behavior of the morphism is undefined. The behavior is also undefined if you provide any Float or Integer number that is out of range of JavaScript engine.

NaN, -Infinity, +Infinity are part of JavaScripts Number type, while I was trying to infer more abstract concept of Number, that is not bound to JavaScript implementation.

Does it make sense ?

Max Barkley
a month ago

But the term morphism typically refers to a thing within a category, not between categories (unless of course you are in a category of categories).

See here that the definition of endomorphism clearly refers to a morphism for an object in a category.

Vladimir Gorej
25 days ago

I hear you, but for example homomorphism is usually used to search for the structural similarities between two categories. I did not used words like typically, because they are pretty vague in the sense that they don’t define the thing precisely. I used exact definitions for each morphism that I was describing.

Regarding endomorphism: what you’re saying is true. In my definition I’m saying exactly the same - take an object/member of category Number, run it through the morphism and the result is again another object/member of the same category.

I am sorry If I misunderstood your comment, but I think that what you’re saying has already been said in the article it self.

Fabian B
a month ago

thx finally understood the different kinds of morphims, it wasn’t clear to me in pure math notation, but the examples made it clear to me now

Vladimir Gorej
a month ago

If you’re interested into more morphisms checkout this repo and specifically this PR: https://github.com/hemanth/functional-programming-jargon/pull/146

Show more replies