Skip to content

Instantly share code, notes, and snippets.

@js-choi
Last active July 10, 2022 22:49
Show Gist options
  • Save js-choi/5b49b350995c5a44571be3d314e38162 to your computer and use it in GitHub Desktop.
Save js-choi/5b49b350995c5a44571be3d314e38162 to your computer and use it in GitHub Desktop.

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:

  1. Array.fromAsync(i) must be equivalent to Array.fromAsync(i, undefined) and Array.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.)

  2. Array.fromAsync(i) must be equivalent to for await (const v of i). (The default case of fromAsync must match intuitions about for await (of), just like how from matches intuitions about for (of).)

  3. Array.fromAsync(i) should be equivalent to AsyncIterator.from(i).toArray().

  4. Array.fromAsync(i, f) should be equivalent to AsyncIterator.from(i).map(f).toArray().

  5. Array.fromAsync(i, f) should conceptually but not strictly be equivalent to Array.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 ].
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment