5

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 }?

1

3 Answers 3

4

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;
    }
}
Sign up to request clarification or add additional context in comments.

Comments

2

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);

4 Comments

Didn't know you could return a yield. That keyword continues to blow my mind. +1 for that.
yield is can be place in any place where a normal expression can be placed AFAIK. It just halts execution and start with the next call to next again.
@conokes, I will add, yield sends a value out of the generator and pauses the execution. If you call .next with a value (like you've done using .next(res), that sends a value back in to the generator – You can think of this incoming value as the "return" of the yield.
@conokes, to be clear, if I return yield foo(), and then I call gen.next(10), the value sent to the return statement is 10.
1

For those who wonder: this is not how the coroutine helper is meant to be used. The function itself should recurse through the wrapperd version, like this:

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);
    });
}

const recurse = coroutine(function* (
  limit = 5, count = 0
) {   
  if(count < limit) {
    yield sleep(100);
    ++count;
    return yield recurse(limit, count);
  } else {
    return count;
  }
});


recurse().then(log).catch(err);

Why?

An asynchronous function is defined as regular function, returning Promise, and never throwing a synchronous exception. The coroutine helper just helps you write asynchronous functions. If you are familiar with async/await from other languages, this wrapper is intended to convert generators to asynchronous functions, where all awaits are replaced by yields. These functions are easier to reason about with this in mind, so people don't have to argue about generators, just asynchronous functions.

2 Comments

Interesting. Can you expand more on why?
Added some explanation.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.