Skip to content

Commit 16d4c77

Browse files
authored
Stage-2 explainer rewrite
1 parent 461d2cc commit 16d4c77

File tree

1 file changed

+159
-66
lines changed

1 file changed

+159
-66
lines changed

README.md

+159-66
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# `Array.fromAsync` for JavaScript
2-
ECMAScript Stage-1 Proposal. J. S. Choi, 2021.
2+
ECMAScript Stage-2 Proposal. J. S. Choi, 2021.
33

44
* **[Specification][]** available
55
* Polyfills:
6-
* **[core-js][]**
76
* **[array-from-async][]**
7+
* **[core-js][]**
88

99
[specification]: http://jschoi.org/21/es-array-async-from/
1010
[core-js]: https://github.com/zloirock/core-js#arrayfromasync
@@ -38,12 +38,16 @@ Further demonstrating the demand for such functionality,
3838
several [Stack Overflow questions][Stack Overflow] have been asked
3939
by various developers, asking how to convert async iterators to arrays.
4040

41+
There are several [real-world examples](#real-world-examples) listed
42+
later in this explainer.
43+
4144
[it-all]: https://www.npmjs.com/package/it-all
4245
[Stack Overflow]: https://stackoverflow.com/questions/58668361/how-can-i-convert-an-async-iterator-to-an-array
4346

4447
## Description
4548
(A [formal draft specification][specification] is available.)
4649

50+
### Async-iterable inputs
4751
Similarly to **[`Array.from`][]**,
4852
**`Array.fromAsync`** would be a **static method**
4953
of the `Array` built-in class, with **one required argument**
@@ -54,82 +58,178 @@ it converts an **async iterable** (or array-like object or iterable)
5458
to a **promise** that will resolve to an array.
5559

5660
```js
57-
async function * f () {
58-
for (let i = 0; i < 4; i++)
59-
yield i;
61+
async function * asyncGen (n) {
62+
for (let i = 0; i < n; i++)
63+
yield i * 2;
6064
}
61-
62-
// Resolves to [0, 1, 2, 3].
63-
await Array.fromAsync(f());
65+
// arr will be [0, 2, 4, 6].
66+
const arr = [];
67+
for await (const v of asyncGen(4)) {
68+
arr.push(v);
69+
}
70+
// This is equivalent.
71+
const arr = await Array.fromAsync(asyncGen(4));
6472
```
6573

66-
`mapfn` is an optional function to call on every item value.
67-
(Unlike `Array.from`, `mapfn` may be an **async function**.
68-
Whenever `mapfn` returns a promise, that promise will be awaited,
69-
and the value it resolves to is what is added
70-
to the final returned promise’s array.
71-
If `mapfn`’s promise rejects,
72-
then the final returned promise
73-
will also reject with that error.)
74-
75-
`thisArg` is an optional value with which to call `mapfn`
76-
(or `undefined` by default).
77-
78-
Like `for await`, when `Array.fromAsync` receives a **sync-iterable object**
79-
(and that object is not async iterable),
80-
then it creates a sync iterator for that object and adds its items to an array.
81-
When **any yielded item is a promise**, then that promise will **block** the iteration
82-
until it **resolves** to a value (in which case that value is what is added to the array)
83-
or until it **rejects** with an error (in which case
84-
the promise returned by `Array.fromAsync` itself will reject with that error).
74+
### Sync-iterable inputs
75+
If the argument is a sync iterable (and not an async iterable), then the return value is still a promise that will resolve to an array.
76+
If the sync iterator yields promises, then each yielded promise is awaited before its value is added to the new array. (Values that are not promises are also awaited for one microtick to prevent Zalgo.)
77+
This matches the behavior of `for await`.
8578

8679
Like `Array.from`, `Array.fromAsync` also works on non-iterable **array-like objects**
8780
(i.e., objects with a length property and indexed elements).
8881
As with sync-iterable objects, any element that is a promise must settle first,
8982
and the value to which it resolves (if any) will be what is added to the resulting array.
9083

91-
Also like `Array.from`, `Array.fromAsync` is a **generic factory method**.
92-
It does not require that its `this` value be the `Array` constructor,
93-
and it can be transferred to or inherited by any other constructors
94-
that may be called with a single numeric argument.
84+
```js
85+
function * genPromises (n) {
86+
for (let i = 0; i < n; i++)
87+
yield Promise.resolve(i * 2);
88+
}
89+
// arr will be [0, 2, 4, 6].
90+
const arr = [];
91+
for await (const v of genPromises(4)) {
92+
arr.push(v);
93+
}
94+
// This is equivalent.
95+
const arr = await Array.fromAsync(genPromises(4));
96+
```
9597

96-
## Other proposals
98+
### Non-iterable array-like inputs
99+
Array.fromAsync’s valid inputs are a superset of Array.from’s valid inputs. This includes non-iterable array-likes: objects that have a length property as well as indexed elements.
100+
The return value is still a promise that will resolve to an array.
101+
If the array-like object’s elements are promises, then each accessed promise is awaited before its value is added to the new array.
102+
One TC39 representative’s opinion: “[Array-likes are] very much not obsolete, and it’s very nice that things aren’t forced to implement the iterator protocol to be transformable into an Array.”
97103

98-
### `Object.fromEntriesAsync`
99-
In the future, a complementary method could be added to `Object`.
104+
```js
105+
const arrLike = {
106+
length: 4,
107+
0: Promise.resolve(0),
108+
1: Promise.resolve(2),
109+
2: Promise.resolve(4),
110+
3: Promise.resolve(6),
111+
}
112+
// arr will be [0, 2, 4, 6].
113+
const arr = [];
114+
for await (const v of Array.from(arrLike)) {
115+
arr.push(v);
116+
}
117+
// This is equivalent.
118+
const arr = await Array.fromAsync(arrLike);
119+
See issue #7. Previously discussed at 2021-11 plenary without objections.
120+
```
100121

101-
Type | Sync method | Async method
102-
------- | ------------ | ------------------
103-
`Array` | `from` | `fromAsync`
104-
`Object`| `fromEntries`| `fromEntriesAsync`?
122+
### Generic factory method
123+
Array.fromAsync is a generic factory method. It does not require that its this receiver be the Array constructor.
124+
fromAsync can be transferred to or inherited by any other constructor with a single numeric parameter. In that case, the final result will be the data structure created by that constructor (with 0 as its argument), and with each value yielded by the input being assigned to the data structure’s numeric properties.
125+
(Symbol.species is not involved at all.)
126+
If the this receiver is not a constructor, then fromAsync creates an array as usual.
127+
This matches the behavior of Array.from.
105128

106-
It is **uncertain** whether `Object.fromEntriesAsync`
107-
should be **piggybacked** onto this proposal
108-
or left to a **separate** proposal.
129+
```js
130+
async function * asyncGen (n) {
131+
for (let i = 0; i < n; i++)
132+
yield i * 2;
133+
}
134+
function Data (n) {}
135+
Data.from = Array.from;
136+
Data.fromAsync = Array.fromAsync;
137+
// d will be a new Data(0), with
138+
// 0 assigned to 0, 1 assigned to 2, etc.
139+
const d = new Data(0); let i = 0;
140+
for await (const v of asyncGen(4)) {
141+
d[i] = v;
142+
}
143+
// This is equivalent.
144+
const d = await Data.fromAsync(asyncGen(4));
145+
```
109146

110-
### Async spread operator
111-
In the future, standardizing an async spread operator (like `[ 0, await ...v ]`)
112-
may be useful. This proposal leaves that idea to a **separate** proposal.
147+
### Optional parameters
148+
Array.fromAsync has two optional parameters.
149+
The first optional parameter is a mapping callback, which is called on each value yielded from the input – the result of which is awaited then added to the array.
150+
Unlike `Array.from`, `mapfn` may be an async function.)
151+
By default, this is essentially an identity function.
152+
The second optional parameter is a this value for the mapping callback. By default, this is undefined.
153+
These optional parameters match the behavior of Array.from. Their exclusion would be surprising to developers who are already used to Array.from.
113154

114-
### Iterator helpers
115-
The **[iterator-helpers][] proposal** puts forward, among other methods,
116-
a **`toArray` method** for async iterators (as well as synchronous iterators).
117-
We **could** consider `Array.fromAsync` to be **redundant** with `toArray`.
155+
```js
156+
async function * asyncGen (n) {
157+
for (let i = 0; i < n; i++)
158+
yield i * 2;
159+
}
160+
// arr will be [0, 4, 16, 36].
161+
const arr = [];
162+
for await (const v of asyncGen(4)) {
163+
arr.push(v ** 2);
164+
}
165+
// This is equivalent.
166+
const arr = await Array.fromAsync(asyncGen(4),
167+
v => v ** 2);
168+
```
169+
170+
### Errors
171+
Like other promise-based APIs, Array.fromAsync will always immediately return a promise. It will never synchronously throw an error and summon Zalgo.
172+
If its input throws an error while creating its async or sync iterator, then its promise will reject with that error.
173+
If its input’s iterator throws an error while yielding a value, then its promise will reject with that error.
174+
If its this receiver’s constructor throws an error, then its promise will reject to that error.
175+
If its mapping callback throws an error when given an input value, then its promise will reject with that error.
176+
If its input is null or undefined, or if its mapping callback is neither undefined nor callable, then its promise will reject with a TypeError.
177+
178+
```js
179+
const err = new Error;
180+
const badIterable = { [Symbol.iterator] () { throw err; } };
181+
function * genError () { throw err; }
182+
function * genRejection () { yield Promise.reject(err); }
183+
function badCallback () { throw err; }
184+
function BadConstructor () { throw err; }
185+
// These create promises that will reject with err.
186+
Array.fromAsync(badIterable);
187+
Array.fromAsync(genError());
188+
Array.fromAsync(genRejection());
189+
Array.fromAsync(genErrorAsync());
190+
Array.fromAsync([], badCallback);
191+
BadConstructor.call(Array.fromAsync, []);
192+
// These create promises that will reject with TypeErrors.
193+
Array.fromAsync(null);
194+
Array.fromAsync([], 1);
195+
```
196+
197+
## Other proposals
198+
199+
### Relationship with iterator-helpers
200+
The [iterator-helpers][] proposal has toArray, which works with both sync and async iterables.
118201

119-
However, **`Array.from` already** exists,
120-
and `Array.fromAsync` would **parallel** it.
121-
If we **had to choose** between `asyncIterator.toArray` and `Array.fromAsync`,
122-
we should **prefer** `Array.fromAsync` to `asyncIterator.toArray`
123-
for its **parallelism** with what already exists.
202+
```js
203+
Array.from(gen())
204+
gen().toArray()
205+
Array.fromAsync(asyncGen())
206+
asyncGen().toArray()
207+
```
208+
209+
toArray overlaps with both Array.from and Array.fromAsync. This is okay. They can coexist.
210+
If we have to choose between having toArray and having fromAsync, then we should choose fromAsync. We already have Array.from. We should match the existing language precedent.
124211

125-
In addition, the `iterator.toArray` method **already would duplicate** `Array.from`
126-
for **synchronous iterators**.
127-
We consider **duplication** with an `Array` method as **okay** anyway.
128-
If duplication between `syncIterator.toArray` and `Array.from` is already okay,
129-
then duplication between `asyncIterator.toArray` and `Array.fromAsync` should also be okay.
212+
See [tc39/proposal-iterator-helpers#156](https://github.com/tc39/proposal-iterator-helpers/issues/156).
213+
A co-champion of iterable-helpers seems to agree that we should have both or that we should prefer Array.fromAsync:
214+
“I remembered why it’s better for a buildable structure to consume an iterable than for an iterable to consume a buildable protocol. Sometimes building something one element at a time is the same as building it [more than one] element at a time, but sometimes it could be slow to build that way or produce a structure with equivalent semantics but different performance properties.”
130215

131216
[iterator-helpers]: https://github.com/tc39/proposal-iterator-helpers
132217

218+
### TypedArray.fromAsync, Set.fromAsync, etc.
219+
The following built-ins also resemble Array.from:
220+
```js
221+
TypedArray.from
222+
new Set
223+
Object.fromEntries
224+
new Map
225+
```
226+
We are deferring any async versions of these methods to future proposals.
227+
See [issue #8](https://github.com/tc39/proposal-array-from-async/issues/8) and [proposal-setmap-offrom](https://github.com/tc39/proposal-setmap-offrom).
228+
229+
### Async spread operator
230+
In the future, standardizing an async spread operator (like `[ 0, await ...v ]`)
231+
may be useful. This proposal leaves that idea to a **separate** proposal.
232+
133233
### Records and tuples
134234
The **[record/tuple] proposal** puts forward two new data types
135235
with APIs that respectively **resemble** those of **`Array` and `Object`**.
@@ -139,21 +239,14 @@ depends on [whether `Object` gets `fromEntriesAsync`](#objectfromentriesasync).
139239

140240
[record/tuple]: https://github.com/tc39/proposal-record-tuple
141241

142-
### Set and Map
143-
There is a [proposal for `Set.from` and `Map.from` methods][setmap-offrom].
144-
If this proposal is accepted before that proposal,
145-
then that proposal could also add corresponding `fromAsync` methods.
146-
147-
[setmap-offrom]: https://github.com/tc39/proposal-setmap-offrom
148-
149242
## Real-world examples
150243
Only minor formatting changes have been made to the status-quo examples.
151244

152245
<table>
153246
<thead>
154247
<tr>
155248
<th>Status quo
156-
<th>With binding
249+
<th>With Array.fromAsync
157250

158251
<tbody>
159252
<tr>

0 commit comments

Comments
 (0)