Suppose I have a function that takes a generator and returns another generator of the first n
elements:
const take = function * (n, xs) {
console.assert(n >= 0);
let i = 0;
for (const x of xs) {
if (i == n) {
break;
}
yield x;
i++;
}
};
Usage like this:
const evens = function * () {
let i = 0;
while (true) {
yield i;
i += 2;
}
};
for (const x of take(10, evens())) {
console.log(x);
}
Now imagine that evens
is also async
(see this answer for setup):
const evensAsync = async function * () {
let i = 0;
while (true) {
yield i;
i += 2;
}
};
This, of course, does not work with take
:
const main = async () => {
for await (const x of take(10, evensAsync())) {
console.log(x);
}
};
main().catch(e => console.error(e));
Now, I can define a take
variant that is async
:
const takeAsync = async function * (n, xs) {
console.assert(n >= 0);
let i = 0;
for await (const x of xs) {
if (i == n) {
break;
}
yield x;
i++;
}
};
const main = async () => {
for await (const x of takeAsync(10, evensAsync())) {
console.log(x);
}
};
main().catch(e => console.error(e));
... but what a hassle!
Is there a way to automatically "asyncify" generator functions in JavaScript?
Is there a way to automatically "asyncify" generator functions in JavaScript?
No. Asynchronous and synchronous generators are just too different. You will need two different implementations of your take
function, there's no way around it.
You can however dynamically select which one to choose:
async function* takeAsync(asyncIterable) { … }
function* takeSync(iterable) { … }
function take(obj) {
if (typeof obj[Symbol.asyncIterator] == "function") return takeAsync(obj);
if (typeof obj[Symbol.iterator] == "function") return takeSync(obj);
throw new TypeError("not an iterable object");
}
It is not in general possible to write a function asyncify
that could be used like asyncify(takeSync)(10, evensAsync())
. One might be able to hack something together that works for takeSync
and mapSync
, relying on the fact that they output exactly one item per input item, but it would be pretty brittle and not work for other iteration functions like filter
.
That said, it is of course possible to generalise and provide abstractions.
function taker(n) {
return {
done: n > 0,
*step(element) { /* if (n > 0) */ yield element; return taker(n-1); },
result: null
}
}
function mapper(fn) {
return {
done: false,
*step(element) { yield fn(element); return this; }
result: null
}
}
function makeIterator(t) {
return function*(iterable) {
for (let element of iterable) {
t = yield* t.step(element);
if (t.done) break;
}
return t.result;
};
}
function makeAsyncIterator(t) {
return async function*(asyncIterable) {
for await (let element of asyncIterable) {
t = yield* t.step(element);
if (t.done) break;
}
return t.result;
};
}
(which shamelessly rips off the transducer concept - also not tested for anything)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With