Search code examples
javascriptrecursionpromisetreees6-promise

Recursion with promises and concurrency


Bear with me, because this takes some setting out.

Consider this trivial tree of values, for the purpose of the question / discussion.

const valuetree = [{
    item: 1,
    value: 100,
  },
  {
    item: 2,
    value: [{
      item: 3,
      value: [{
          item: 4,
          value: 200
        },
        {
          item: 5,
          value: 200
        },
        {
          item: 6,
          value: 200
        },
      ]
    }]
  },
];

Now if we wanted to write a recursive function to add up all the values, then this in itself is fairly trivial. Like so :

function sumTree(leaf) {
  let total = 0;

  if (Array.isArray(leaf)) {
    leaf.forEach(subLeaf => {
      total += sumTree(subLeaf);
    });
  } else {
    if (Array.isArray(leaf.value)) {
      total += sumTree(leaf.value);
    } else {
      total += Number(leaf.value);
    }
  }

  return total;
}

Now suppose we want to use promises to achieve the same thing?

This is what I end up with ...

async function sumTreeAsync(leaf) {
  let promises = [];

  if (Array.isArray(leaf)) {
    leaf.forEach(async subLeaf => {
      promises.push(sumTreeAsync(subLeaf));
    });
  } else {
    if (Array.isArray(leaf.value)) {
      promises.push(sumTreeAsync(leaf.value));
    } else {
      promises.push(leaf.value);
    }
  }

  return promises;
}

So what gets returned looks like this

[ Promise { [ 100 ] }, Promise { [ [Promise] ] } ]

Which makes sense, an array of 2 top level items, a promise of a literal value, and a promise that returns a nested array of promises.

So now as some function like Promise.all only deals with a flat array of promises, I would have to recurse the nested promises to arrive at the same outcome. So now I have to recurse the tree twice, which just smells so bad.

So perhaps I need to return resolved promises from the 2 cases where the subleaf is an array?

Is it even worth doing this or should recursion just be synchronous?


Solution

  • You're not far off. If you change your return to do a Promise .all call and .then (sum) for an obvious sum function, this would work:

    const sum = (ns) => ns .reduce ((a, b) => a + b, 0)
    
    async function sumTreeAsync(leaf) {
      let promises = [];
    
      if (Array.isArray(leaf)) {
        leaf.forEach(async subLeaf => {
          promises.push(sumTreeAsync(subLeaf));
        });
      } else {
        if (Array.isArray(leaf.value)) {
          promises.push(sumTreeAsync(leaf.value));
        } else {
          promises.push(leaf.value);
        }
      }
    
      return Promise.all(promises).then(sum);
    }
    
    const valuetree = [{item: 1, value: 100}, {item: 2, value: [{item: 3, value: [{item: 4, value: 200}, {item: 5, value: 200}, {item: 6, value: 200}]}]}];
    
    sumTreeAsync (valuetree)
      .then (console .log)

    But I would definitely write this code in a different way:

    const sum = (ns) => ns .reduce ((a, b) => a + b, 0)
    
    const sumTreeAsync = async (leaf) =>
      Promise .all (Array .isArray (leaf)
        ? leaf.map (sumTreeAsync)
        : Array .isArray (leaf .value)
          ? [sumTreeAsync (leaf .value)]
          : [leaf .value]
      ) .then (sum)
    
    const valuetree = [{item: 1, value: 100}, {item: 2, value: [{item: 3, value: [{item: 4, value: 200}, {item: 5, value: 200}, {item: 6, value: 200}]}]}];
    
    sumTreeAsync (valuetree)
      .then (console .log)

    More than that. I would probably not write this code at all. JS is still generally a single-threaded language. So you get no concurrent calculations at all in this approach. If instead of your simple summation, you were handing things off to different workers to process, then it might make sense. As it is, this just feels like an unnecessary complication.