Home > database >  Why does using flatMap with an async function not return a flatten array?
Why does using flatMap with an async function not return a flatten array?

Time:12-30

I want to understand why using .flatMap() with async does not return a flatten array.

For example, for Typescript, this returns an array of numbers: I know Promisse.all and async are useless with a simple array of number, it is just easier for reproduction

const numbers = [[1], [2], [3]];
// typed as number[]
const numbersFlatten = await Promise.all(
    numbers.flatMap((number) => number),
);

When this, for Typescript, returns an array of array of numbers (just added an async) :

const numbers = [[1], [2], [3]];
// typed as number[][]
const numbersFlatten = await Promise.all(
    numbers.flatMap(async (number) => number),
);

CodePudding user response:

All async functions implicitly return Promises. By making your .flatMap() callback async, it now returns a Promise that resolves to the number array. For .flatMap() to work correctly and for it to flatten its results, the callback should return an array, not a Promise:

const numbers = [[1], [2], [3]];
const promiseAllArg = numbers.flatMap(async (number) => number); // same as `.flatMap(number => Promise.resolve(number))`, flatMap doesn't know how to flatten `Promise<number>` into a resulting array
console.log(promiseAllArg); // [Promise<[1]>, Promise<[2]>, Promise<[3]>]

Instead, you can use a regular .map() call to obtain a nested array of resolved values followed by a .flat() call:

(async () => {
  const numbers = [[1], [2], [3]];
  const numbersFlatten = (await Promise.all(
    numbers.map(async (number) => number)
  )).flat() // flatten the resolved values
  console.log(numbersFlatten);
})();

CodePudding user response:

I wanted to add that the fact Promise is eager also has performance implications.

const asyncFlatMap = <A, B>(arr: A[], f: (a: A) => Promise<B>) =>
    Promise.all(arr.map(f)).then(arr => arr.flat());

asyncFlatMap([[1], [2], [3]], async nums => nums); // Promise<number[]>

You can see in the code above that we need to map, then flat, with this additional round trip through the microtasks queue because of the then in-between. Doing this kind of round trip for the sake of playing Lego is wasteful.

If you happen to nest promises a lot you may be interested in Fluture.

Futures are lazy, so you can play Lego with them before anything is run.

import { resolve, parallel, promise, map } from 'fluture';
import { pipe } from 'fp-ts/function';

const numbersFlatten = await pipe(
    numbers.map(nums => resolve(nums)), // Future<number[]>[]
    parallel(10), // Future<number[][]>
    map(x => x.flat()), // Future<number[]>
    promise // Promise<number[]>
);

In the code above I converted the Future back to a Promise with promise in order to match your use case. It is only when this function is called that the computation is run. fork would also run the computation without ever involving promises.

In case you are not familiar with pipe: the first argument is the value which is threaded through the remaining function arguments.

The magic value 10 in parallel is just the maximum number of computations to run concurrently. It could have been anything else.

  • Related