Open getify opened 6 years ago
I like the sound of that, I'd be keen to have a go at implementing something - I've been working on a related thing myself called itrabble and am looking forward to async generators being available for building in that capability.
As for those points to consider, I would (in my very inexperienced experience) expect
I'm still getting up to speed on JavaScript generators and fasy itself, but here are my thoughts at the moment:
yield
ed promises?
I would assume "Yes", as that would make it consistent with how fasy handles promises normally (at least as far as I understand it at this point).return
ed values?
Seeing as normal generators throw away return
values, my gut feeling is "no", otherwise you start to depart from the standard expectation of how the generator would function. One question I have though is whether one should wait on a return
ed promise
, even if you ultimately throw the result away after it resolves. I'm leaning towards "yes".generator
s with no return
?
If the answer to #2 is "no", this would naturally follow suit and be "no" as well.Upon further reflection, I think a bit differently from how I thought earlier.
I think these answers come down to, would/should someone using an async function *
generator with fasy be treating it more like an async function
or like a function*
?
in async generators you're supposed to await
promises, not yield
them. If an async generator is used, the ability to yield
from them seems mis-guided (with fasy, specifically) and thus shouldn't be covered up with magic. Therefore, we shouldn't do the magical wait-on-promise there.
We have two other options then:
yield
ed, even a promise, is just sent back in to the generator immediately.yield
ed is collected into a list of values, which is eventually the final result of that function. IOW, if yield 1
, yield 2
, and yield 3
all run, then the final result of that function is a list of [1,2,3]
. This idea is consistent with thinking more about it as a generator, which produces multiple values via yield
.But to the initial classification question above, when using fasy, I think async function*
is more async function
than function *
. As such, a return
value is the generally expected way to give a result value from an async function
. So, any return
ed value, even the default undefined
, is the result.
But how then does this rule mix with the idea of multiple yield
results being collected into a list? My inclination is, if we affirm this rule (2), it would be incompatible with that idea, and therefore that one would be eliminated.
This one would suggest a way to allow a compromise for that previous question, instead of forcing it to be eliminated -- a list of yield
ed values, if any, can be overridden as the final result, but only by an explicit non-undefined
value being otherwise returned
.
That could work, but I no longer think this sort of magic is a good idea. I think this rule should be no.
@getify My two cents on point one: if an async generator yield
s a promise
, it is automagically awaited:
async function * PromiseYielder() {
var i = 0;
while(true) {
yield new Promise(ok => ok(i++));
}
};
;(async () => {
const ait = PromiseYielder();
(await ait.next()).value; // 0 not Promise<0>
(await ait.next()).value; // 1 not Promise<1>
})();
The more I've thought about this, the more I think we have to treat async generators as async functions that happen to also have a yield
, which if used behaves the same as await
.
The earlier thinking I had was that it being a generator offered an interesting opportunity to support lazy iteration protocols, but fundamentally fasy is about eager iterations. A different set of functions should be provided for lazy iteration.
@getify of course generators are the fulcrum of the lazy iteration in JavaScript (although they are often not used in this way), and that is the main reason I cannot make equivalent yield
with await
.
We could argue about the fact that both pause the execution of a function and both let multiple values be inserted into it and extracted from it.
But is thanks to yield
that the control of the execution could be "gifted" to other entities in the code, enabling the above-mentioned lazy iteration. This is not what await
is for: considering the async/await
pattern as the union of promises, generators and functions like your _runner
, is easy to see that the control is taken by runner functions that restart the paused function as soon as possible. This is eager iteration.
Moreover, even if it is not so important, the yield
keyword has less precedence than the await
operator:
// not valid js
function * gen() {
yield 1 + yield 2;
}
// valid js
async function af() {
await 1 + await 2;
}
On the other hand, coming back to async function *
s, we could not ignore the fact that a yield
ed promise is await
ed as the await
keyword was used. This make sense because if it were not so, we would have ended up too close to nested promises (or promise of a promise if you prefer): the promise always returned by the async iteration and the promise inside the value
field.
So, what about your question?
I've not spent much time with the internals of fasy, but of course is evident that is based on eager iteration. And change its nature to support lazy iteration for async generators feels wrong. Even because fasy already ignore the fact that also sync generators could be lazy iterated, you simply handle them with _runner
.
Considering the eager iteration fact, I think that fasy should handle a case like the following:
var users = [ "bzmau", "getify", "frankz" ];
FA.serial.map(
async function *getOrders(username){
var user = await lookupUser( username );
return lookupOrders( user.id );
},
users
)
.then( userOrders => console.log( userOrders ) );
or:
var users = [ "bzmau", "getify", "frankz" ];
FA.serial.map(
async function *getOrders(username){
var user = yield lookupUser( username );
return lookupOrders( user.id );
},
users
)
.then( userOrders => console.log( userOrders ) );
as:
var users = [ "bzmau", "getify", "frankz" ];
FA.serial.map(
function *getOrders(username){
var user = yield lookupUser( username );
return lookupOrders( user.id );
},
users
)
.then( userOrders => console.log( userOrders ) );
In other words: an async generator is a generator that happen to also have an await
keyword.
IMO you should "run" an async generator like a generator, feeding it with any value that is async yield
ed (here it is the eager iteration) and taking the returned value as the result.
This is what I expect to see, this is what seems more logical considering the eager nature of fasy.
Async-generators are stage3, so fairly likely to land in JS. Seems like fasy should support them eventually, probably sooner than later.
[UPDATE]: They landed in ES2018.
For example:
Implementation should be fairly straightforward, in
_runner(..)
.Note: I don't think the
for-await-of
loop will work to run the async-generator, because that construct doesn't let us send a "next value" back in after each iteration. But I think we can do afor-of
loop withawait
inside it, to get almost the same syntax-sugar but have the capability to drive the iterator as we need to. We'll look forSymbol.asyncIterator
to know that we need to take this path.However, before proceeding, we need to decide:
yield
s out a promise, do we wait on it and resume with its resolution, like we do in the normal generator-runner pattern?_runner(..)
)? Usually,return
d values from a generator would be thrown awayreturn
orreturn undefined
), should we assume the lastyield
ed value (or its resolution, if it was a promise) as the overall completion of that function's promise (in_runner(..)
)?