Generator Coroutine

Hopefully Chapter 4 helped you get pretty familiar with ES6 generators. In particular, we want to revisit the “Generator Concurrency” discussion, and push it even further.

We imagined a runAll(..) utility that could take two or more generators and run them concurrently, letting them cooperatively yield control from one to the next, with optional message passing.

In addition to being able to run a single generator to completion, the ASQ#runner(..) we discussed in Appendix A is a similar implementation of the concepts of runAll(..), which can run multiple generators concurrently to completion.

So let’s see how we can implement the concurrent Ajax scenario from Chapter 4:

  1. ASQ(
  2. "http://some.url.2"
  3. )
  4. .runner(
  5. function*(token){
  6. // transfer control
  7. yield token;
  8. var url1 = token.messages[0]; // "http://some.url.1"
  9. // clear out messages to start fresh
  10. token.messages = [];
  11. var p1 = request( url1 );
  12. // transfer control
  13. yield token;
  14. token.messages.push( yield p1 );
  15. },
  16. function*(token){
  17. var url2 = token.messages[0]; // "http://some.url.2"
  18. // message pass and transfer control
  19. token.messages[0] = "http://some.url.1";
  20. yield token;
  21. var p2 = request( url2 );
  22. // transfer control
  23. yield token;
  24. token.messages.push( yield p2 );
  25. // pass along results to next sequence step
  26. return token.messages;
  27. }
  28. )
  29. .val( function(res){
  30. // `res[0]` comes from "http://some.url.1"
  31. // `res[1]` comes from "http://some.url.2"
  32. } );

The main differences between ASQ#runner(..) and runAll(..) are as follows:

  • Each generator (coroutine) is provided an argument we call token, which is the special value to yield when you want to explicitly transfer control to the next coroutine.
  • token.messages is an array that holds any messages passed in from the previous sequence step. It’s also a data structure that you can use to share messages between coroutines.
  • yielding a Promise (or sequence) value does not transfer control, but instead pauses the coroutine processing until that value is ready.
  • The last returned or yielded value from the coroutine processing run will be forward passed to the next step in the sequence.

It’s also easy to layer helpers on top of the base ASQ#runner(..) functionality to suit different uses.

State Machines

One example that may be familiar to many programmers is state machines. You can, with the help of a simple cosmetic utility, create an easy-to-express state machine processor.

Let’s imagine such a utility. We’ll call it state(..), and will pass it two arguments: a state value and a generator that handles that state. state(..) will do the dirty work of creating and returning an adapter generator to pass to ASQ#runner(..).

Consider:

  1. function state(val,handler) {
  2. // make a coroutine handler for this state
  3. return function*(token) {
  4. // state transition handler
  5. function transition(to) {
  6. token.messages[0] = to;
  7. }
  8. // set initial state (if none set yet)
  9. if (token.messages.length < 1) {
  10. token.messages[0] = val;
  11. }
  12. // keep going until final state (false) is reached
  13. while (token.messages[0] !== false) {
  14. // current state matches this handler?
  15. if (token.messages[0] === val) {
  16. // delegate to state handler
  17. yield *handler( transition );
  18. }
  19. // transfer control to another state handler?
  20. if (token.messages[0] !== false) {
  21. yield token;
  22. }
  23. }
  24. };
  25. }

If you look closely, you’ll see that state(..) returns back a generator that accepts a token, and then it sets up a while loop that will run until the state machine reaches its final state (which we arbitrarily pick as the false value); that’s exactly the kind of generator we want to pass to ASQ#runner(..)!

We also arbitrarily reserve the token.messages[0] slot as the place where the current state of our state machine will be tracked, which means we can even seed the initial state as the value passed in from the previous step in the sequence.

How do we use the state(..) helper along with ASQ#runner(..)?

  1. var prevState;
  2. ASQ(
  3. /* optional: initial state value */
  4. 2
  5. )
  6. // run our state machine
  7. // transitions: 2 -> 3 -> 1 -> 3 -> false
  8. .runner(
  9. // state `1` handler
  10. state( 1, function *stateOne(transition){
  11. console.log( "in state 1" );
  12. prevState = 1;
  13. yield transition( 3 ); // goto state `3`
  14. } ),
  15. // state `2` handler
  16. state( 2, function *stateTwo(transition){
  17. console.log( "in state 2" );
  18. prevState = 2;
  19. yield transition( 3 ); // goto state `3`
  20. } ),
  21. // state `3` handler
  22. state( 3, function *stateThree(transition){
  23. console.log( "in state 3" );
  24. if (prevState === 2) {
  25. prevState = 3;
  26. yield transition( 1 ); // goto state `1`
  27. }
  28. // all done!
  29. else {
  30. yield "That's all folks!";
  31. prevState = 3;
  32. yield transition( false ); // terminal state
  33. }
  34. } )
  35. )
  36. // state machine complete, so move on
  37. .val( function(msg){
  38. console.log( msg ); // That's all folks!
  39. } );

It’s important to note that the *stateOne(..), *stateTwo(..), and *stateThree(..) generators themselves are reinvoked each time that state is entered, and they finish when you transition(..) to another value. While not shown here, of course these state generator handlers can be asynchronously paused by yielding Promises/sequences/thunks.

The underneath hidden generators produced by the state(..) helper and actually passed to ASQ#runner(..) are the ones that continue to run concurrently for the length of the state machine, and each of them handles cooperatively yielding control to the next, and so on.

Note: See this “ping pong” example (http://jsbin.com/qutabu/1/edit?js,output) for more illustration of using cooperative concurrency with generators driven by ASQ#runner(..).