I think @bakkot gives some persuasive points, especially that the mapping function is actually essentially an async function, so it wouldn’t make sense for its identity to be x => x
.
My priorities, in order, have always been:
-
Array.fromAsync(i)
must be equivalent toArray.fromAsync(i, undefined)
andArray.fromAsync(i, null)
. (For optional parameters, nullish arguments should be functionally equivalent to omitting the arguments. This is how every function in the core language is designed, and I believe it is also an explicit best practice in Web APIs.) -
Array.fromAsync(i)
must be equivalent tofor await (const v of i)
. (The default case offromAsync
must match intuitions aboutfor await (of)
, just like howfrom
matches intuitions aboutfor (of)
.) -
Array.fromAsync(i)
should be equivalent toAsyncIterator.from(i).toArray()
. -
Array.fromAsync(i, f)
should be equivalent toAsyncIterator.from(i).map(f).toArray()
. -
Array.fromAsync(i, f)
should conceptually but not strictly be equivalent toArray.from(i, f)
.
I lost sight of the second priority when I wrote #20.
Bakkot points out that the default mapping function of Array.fromAsync does not have to be x => x
, and omitting the mapping function does not have to be equivalent to specifying x => x
or some other function.
Therefore, I plan to revert my changes in #20. The default behavior without a mapping function will be to not await values yielded by async iterators. When a mapping function is given, the inputs supplied to the mapping function will be the values yielded by the input async iterator without awaiting; only the results of the mapping function will be awaited. This behavior should match AsyncIterator.prototype.toArray
.
function createIt () {
return {
[Symbol.asyncIterator]() {
let i = 1;
return {
async next() {
if (i > 2) {
return { done: true };
}
i++;
return { value: Promise.resolve(i), done: false }
},
};
},
};
}
Without any mapping function:
result = [];
for await (const x of createIt()) {
console.log(x);
result.push(x);
}
// result is [ Promise.resolve(1), Promise.resolve(2), Promise.resolve(3) ].
result = await Array.fromAsync(createIt());
// result is [ Promise.resolve(1), Promise.resolve(2), Promise.resolve(3) ].
With mapping function x => x
:
result = await Array.fromAsync(createIt(), x => x);
// result is [ 1, 2, 3 ].
result = await AsyncIterator.from(createIt())
.map(x => x)
.toArray();
// result is [ 1, 2, 3 ].
With mapping function x => (console.log(x), x)
:
result = await Array.fromAsync(createIt(), x =>
(console.log(x), x));
// Prints three promises.
// result is [ 1, 2, 3 ].
result = await AsyncIterator.from(createIt())
.map(x => (console.log(x), x))
.toArray();
// Prints three promises.
// result is [ 1, 2, 3 ].
With mapping function async x => (console.log(await x), await x))
:
result = await Array.fromAsync(createIt(), async x =>
(console.log(await x), await x));
// Prints three promises.
// result is [ 1, 2, 3 ].
result = await AsyncIterator.from(createIt())
.map(async x => (console.log(await x), await x))
.toArray();
// Prints 1, 2, then 3.
// result is [ 1, 2, 3 ].