-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Should it await next.value
when not passing a mapper function?
#19
Comments
I just noticed that the readme says (emphasis is mine)
|
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. |
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. |
Iteration helpers do not In I think that it should be agreed upon between both proposals. |
@js-choi it's better to leave it open until aligning it with the iterator helpers proposal. |
For what it’s worth, the current spec (as of #20) causes each input value in |
I'm hoping that this bit of code clarifies what this issue is about. // Draft polyfill
Array.asyncFrom = async function(iterable, mapFn = x => x, thisArg = null) {
const result = [];
for await (const value of iterable) {
// NOTE: If the iterable produces a Promise, `value` will be its resolved
// value, because the for-await syntax awaits it.
// See: https://github.com/tc39/proposal-async-iteration/issues/15
const mapFnResult = mapFn.call(thisArg, value);
// This issue (https://github.com/tc39/proposal-array-from-async/issues/19)
// is about deciding whether the `await` should appear here or not.
result.push(await mapFnResult);
}
return result;
} Assuming I haven't gotten this wrong, I think we should have the |
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. |
I mean the way I see it there's only really 5 options: Only await mapped values if they are promise-likeThis requires a check for a 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 Have
|
This seems like the right answer to me. And I am OK with having |
Note that let i = 0;
let it = {
[Symbol.asyncIterator]() {
return {
async next() {
if (i > 2) return { done: true };
i++;
return { value: Promise.resolve(i), done: false }
}
}
}
};
(async () => {
for await (let v of it) {
console.log(v); // prints a promise
}
})(); I really think we should match that behavior when not passing the second argument. |
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 My priorities, in order, have always been:
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 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 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 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 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 result = await Array.fromAsync(createIt(), async x =>
(console.log(await x), await x));
// Prints 1, 2, then 3.
// 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 ]. |
Consider this async iterable:
Unless I'm reading the spec wrong,
await Array.fromAsync(it)
returns[Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)]
whileawait 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 toArray.from(something, x => x)
. However, there is no "default function" that can be passed toArray.fromAsync
.If we changed
Array.fromAsync(something)
to await thenext.value
values,await Array.fromAsync(it)
would always return the same result asawait Array.fromAsync(it, x => x)
. In practice, it means making it behave likeThe text was updated successfully, but these errors were encountered: