I'm playing around with a recursive generator function that returns values asynchronously. I'm using a coroutine wrapper function to call it. Code and JSBin below:
http://jsbin.com/nuyovay/edit?js,console
let log = console.log.bind(console);
let err = console.error.bind(console);
function coroutine(generatorFn){
return function co() {
let generator = generatorFn.apply(this, arguments);
function handle(result) {
console.log(result);
if (result.done) {
return Promise.resolve(result.value);
}
return Promise.resolve(result.value)
.then(
res => handle(generator.next(res)),
err => handle(generator.throw(err))
);
}
try {
return handle(generator.next());
} catch (err) {
return Promise.reject(err);
}
};
}
function sleep(dur) {
return new Promise(res => {
setTimeout(() => { res() }, dur);
});
}
function* recurse(limit = 5, count = 0) {
if(count < limit) {
yield sleep(100).then(() => Promise.resolve(++count));
yield* recurse(limit, count);
}
else {
return count;
}
}
let test = coroutine(recurse);
test().then(log).catch(err);
Running this returns:
Object {value: Promise, done: false}
Object {value: Promise, done: false}
Object {value: Promise, done: false}
Object {value: Promise, done: false}
Object {value: Promise, done: false}
// `value` should be 5
Object {value: undefined, done: true}
How come the final return
from the generator is undefined
? When I adapt the above for use with bluebird's Promise.coroutine
, I get the same result. Am I missing something fundamental about recursive generators? How do I get it to { value: 5, done: true }
?
A return statement in a generator, when executed, will make the generator finish (i.e. the done property of the object returned by it will be set to true ). If a value is returned, it will be set as the value property of the object returned by the generator.
The yield keyword pauses generator function execution and the value of the expression following the yield keyword is returned to the generator's caller. It can be thought of as a generator-based version of the return keyword. yield can only be called directly from the generator function that contains it.
Yes you can have recursive generators. However, they suffer from the same recursion depth limit as other recursive functions.
When called, generator functions do not initially execute their code. Instead, they return a special type of iterator, called a Generator. When a value is consumed by calling the generator's next method, the Generator function executes until it encounters the yield keyword.
The issue is that you are returning count
, but you're returning it in the parent generator. Unlike yield
in delegated generators, return
is not yielded back up through the delegation chain automatically.
If you want to get the return
value of a delegated generator, you have to assign it directly in the parent generator:
let returnValue = yield* recurse(limit, count);
Since you're using "recursive" generators (multiple levels of delegation), you would need to repeat the process and return the value at every level of delegation:
function* recurse(limit = 5, count = 0) {
if(count < limit) {
yield sleep(100).then(() => Promise.resolve(++count));
let result = yield* recurse(limit, count); // save the return value
return result; // return it to the parent
}
else {
return count;
}
}
In the if
you only have a return
on one side.
You also don't need to use the .then
inside your generator. The whole point of using the generator is so that you don't have to touch the promise API within.
Instead, call recurse
with count + 1
function* recurse(limit = 5, count = 0) {
if(count < limit) {
yield sleep(1000).then(() => Promise.resolve(++count));
return yield* recurse(limit, count + 1);
}
else {
return count;
}
}
And since you're using ES6, while we're at it …
return function co() {
let generator = generatorFn.apply(this, arguments);
… is better off as …
return function co(...args) {
let generator = generatorFn(...args)
all together now
Run the snippet and you'll see the correct output right here
let log = console.log.bind(console);
let err = console.error.bind(console);
function coroutine(generatorFn){
return function co(...args) {
let generator = generatorFn(...args)
function handle(result) {
console.log(result);
if (result.done) {
return Promise.resolve(result.value);
}
return Promise.resolve(result.value)
.then(
res => handle(generator.next(res)),
err => handle(generator.throw(err))
);
}
try {
return handle(generator.next());
} catch (err) {
return Promise.reject(err);
}
};
}
function sleep(dur) {
return new Promise(res => {
setTimeout(() => { res() }, dur);
});
}
function* recurse(limit = 5, count = 0) {
if(count < limit) {
yield sleep(100)
return yield* recurse(limit, count + 1);
}
else {
return count;
}
}
let test = coroutine(recurse);
test().then(log).catch(err);
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