fasy
fasy copied to clipboard
Feature: support async-generators (`async function*`)
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:
FP.serial.map(async function *mapper(v){
if (await lookup(v)) v = yield something(v);
return v;
});
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 a for-of
loop with await
inside it, to get almost the same syntax-sugar but have the capability to drive the iterator as we need to. We'll look for Symbol.asyncIterator
to know that we need to take this path.
However, before proceeding, we need to decide:
- [ ] if the async-generator
yield
s out a promise, do we wait on it and resume with its resolution, like we do in the normal generator-runner pattern? - [ ] if the async-generator has a final return value, is that indeed the overall completion value of that function's promise (in
_runner(..)
)? Usually,return
d values from a generator would be thrown away - [ ] if the async-generator does not have a return value (or if it's
return
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(..)
)?
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
- [x] yes
- [x] I'd suggest yes so that the promise's return value (if there is one) is able to be used subsequently
- [ ] I'm not so sure about this last one, but in order to be consistent with the above I'd expect it to return a promise over the value of whatever gets returned (or not) from its final calling. I may well be wrong about how this works, I'm just going from my understanding of the current async/await usage. Does that make sense?
I'm still getting up to speed on JavaScript generators and fasy itself, but here are my thoughts at the moment:
- [x] Waiting on
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). - [ ] Keep
return
ed values? Seeing as normal generators throw awayreturn
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 areturn
edpromise
, even if you ultimately throw the result away after it resolves. I'm leaning towards "yes". - [ ] Handling
generator
s with noreturn
? 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, notyield
them. If an async generator is used, the ability toyield
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:
- any value that is
yield
ed, even a promise, is just sent back in to the generator immediately. - any value that is
yield
ed is collected into a list of values, which is eventually the final result of that function. IOW, ifyield 1
,yield 2
, andyield 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 viayield
.
- any value that is
-
But to the initial classification question above, when using fasy, I think
async function*
is moreasync function
thanfunction *
. As such, areturn
value is the generally expected way to give a result value from anasync function
. So, anyreturn
ed value, even the defaultundefined
, 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 otherwisereturned
.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.