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.