Description
Consider this async iterable:
const it = {
[Symbol.asyncIterator]() {
return {
async next() {
if (i > 2) return { done: true };
i++;
return { value: Promise.resolve(i), done: false }
}
}
}
}
Unless I'm reading the spec wrong, await Array.fromAsync(it)
returns [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)]
while await Array.fromAsync(it, x => x)
returns [1, 2, 3]
.
Currently, when using Array.from
, x => x
is the "default mapper function" (even if it's not specified as such): Array.from(something)
is always equivalent to Array.from(something, x => x)
. However, there is no "default function" that can be passed to Array.fromAsync
.
If we changed Array.fromAsync(something)
to await the next.value
values, await Array.fromAsync(it)
would always return the same result as await Array.fromAsync(it, x => x)
. In practice, it means making it behave like
const arr = [];
for await (const nextValue of it) arr.push(await nextValue);
Activity
nicolo-ribaudo commentedon Jan 3, 2022
I just noticed that the readme says (emphasis is mine)
ljharb commentedon Jan 3, 2022
If the default is identity, then explicitly passing identity must of course produce the same values. My intuition is that if the explicit identity awaits, then the implicit one also should.
js-choi commentedon Jan 3, 2022
Yes, good catch; I agree. The value of each iteration should be awaited when no mapping function is given. This is a spec bug, and I will fix it soon.
spec: Await next value with default map ƒ
zloirock commentedon Jan 5, 2022
Iteration helpers do not
await
in similar cases.In
for-await-of
,nextValue
is a promise, required explicit awaiting.I think that it should be agreed upon between both proposals.
IteratorValue
in%AsyncIteratorPrototype%
methods tc39/proposal-iterator-helpers#168zloirock commentedon Jan 5, 2022
tc39/proposal-iterator-helpers#168
spec: Await next values with default map ƒ
spec: Await next values with default map ƒ (#20)
zloirock commentedon Jan 5, 2022
@js-choi it's better to leave it open until aligning it with the iterator helpers proposal.
4 remaining items
brad4d commentedon Jan 29, 2022
I'm hoping that this bit of code clarifies what this issue is about.
Assuming I haven't gotten this wrong, I think we should have the
await
shown above, because that is the most consistent with the behavior offor-await
, which awaits the individual elements it is iterating over.js-choi commentedon Jan 29, 2022
Some representatives at the plenary a few days ago stated that they wanted to avoid double awaiting and optimize for the more-common case of omitting the mapping function, so we will need to revisit this issue.
At the very least, we will need to do a thorough comparison of every choice we have, before presenting to plenary again.
RubyTunaley commentedon Mar 28, 2022
I mean the way I see it there's only really 5 options:
Only await mapped values if they are promise-like
This requires a check for a
then
property with type 'function' on each returned mapped value, but if omittingmapfn
is like 95% of cases then this might be fine, since whenundefined
is passed the runtime can branch into a fast path that doesn't do that check.The type check is there to alleviate this concern:
- JSC, TC39 Meeting Notes, January 2022
This solution seems to be the one the committee was leaning towards, although I'm unsure whether they would be happy with the behaviour of a
mapfn
likex => Math.random() < 0.5 ? x : Promise.resolve(x)
.Have
null
andundefined
mean different thingsmapfn
parameter isundefined
, execute with no mapping and without double-await.mapfn
parameter is null, setmapfn
tox => x
.Array.from
currently doesn't acceptnull
formapfn
so it would be a good idea to modify it so that it does too, although in that case it can have the same behaviour asundefined
.Split the function
The function could be split into
Array.fromAsync
and something likeArray.fromAsyncMap
, but that's inconsistent with howArray.from
works and it can't be fixed without either breaking the web or breaking the parameter symmetry betweenfrom
andfromAsync
.Drop mapping
Just get developers to write
(await Array.fromAsync(blah)).map(x => x)
(or with iterator-helpersawait Array.fromAsync(blah.map(x => x))
).Are there any stats available for how often
Array.from
'smapfn
parameter is even used?Accept either double-awaiting everything or having
undefined
behave differently fromx => x
.The committee has made its opinion clear on double-awaiting everything, but having
undefined
behave differently is unintuitive (which is why thenull
option exists).bakkot commentedon Jul 6, 2022
This seems like the right answer to me. And
Array.fromAsync(it, x => { console.log(x); return 0; })
would print three Promises and return[0, 0, 0]
.I am OK with having
undefined
behave differently fromx => x
, becausex => x
isn't acting as the identity function here - its result is being awaited. (One way to look at this is that the second argument isan async function[edit: rather, to be precise, it's a function composed withawait
], even if you happen to write it as a sync function, and there isn't an identity async function.)bakkot commentedon Jul 6, 2022
Note that
for await
does not await these values:I really think we should match that behavior when not passing the second argument.
yield *
in an async iterator should not call.throw
when the inner iterator produces a rejected promise tc39/ecma262#2813throw
in corner case in async generators tc39/ecma262#2818js-choi commentedon Jul 10, 2022
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 specifyingx => 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
.Without any mapping function:
With mapping function
x => x
:With mapping function
x => (console.log(x), x)
:With mapping function
async x => (console.log(await x), await x))
:Revert #20 and do not await async inputs’ values without mapping ƒ
fix: Update explainer to not await values twice
docs: Update history/explainer with resolution of #19