Iterable Sequences

We introduced asynquence‘s iterable sequences in the previous appendix, but we want to revisit them in more detail.

To refresh, recall:

  1. var domready = ASQ.iterable();
  2. // ..
  3. domready.val( function(){
  4. // DOM is ready
  5. } );
  6. // ..
  7. document.addEventListener( "DOMContentLoaded", domready.next );

Now, let’s define a sequence of multiple steps as an iterable sequence:

  1. var steps = ASQ.iterable();
  2. steps
  3. .then( function STEP1(x){
  4. return x * 2;
  5. } )
  6. .then( function STEP2(x){
  7. return x + 3;
  8. } )
  9. .then( function STEP3(x){
  10. return x * 4;
  11. } );
  12. steps.next( 8 ).value; // 16
  13. steps.next( 16 ).value; // 19
  14. steps.next( 19 ).value; // 76
  15. steps.next().done; // true

As you can see, an iterable sequence is a standard-compliant iterator (see Chapter 4). So, it can be iterated with an ES6 for..of loop, just like a generator (or any other iterable) can:

  1. var steps = ASQ.iterable();
  2. steps
  3. .then( function STEP1(){ return 2; } )
  4. .then( function STEP2(){ return 4; } )
  5. .then( function STEP3(){ return 6; } )
  6. .then( function STEP4(){ return 8; } )
  7. .then( function STEP5(){ return 10; } );
  8. for (var v of steps) {
  9. console.log( v );
  10. }
  11. // 2 4 6 8 10

Beyond the event triggering example shown in the previous appendix, iterable sequences are interesting because in essence they can be seen as a stand-in for generators or Promise chains, but with even more flexibility.

Consider a multiple Ajax request example — we’ve seen the same scenario in Chapters 3 and 4, both as a Promise chain and as a generator, respectively — expressed as an iterable sequence:

  1. // sequence-aware ajax
  2. var request = ASQ.wrap( ajax );
  3. ASQ( "http://some.url.1" )
  4. .runner(
  5. ASQ.iterable()
  6. .then( function STEP1(token){
  7. var url = token.messages[0];
  8. return request( url );
  9. } )
  10. .then( function STEP2(resp){
  11. return ASQ().gate(
  12. request( "http://some.url.2/?v=" + resp ),
  13. request( "http://some.url.3/?v=" + resp )
  14. );
  15. } )
  16. .then( function STEP3(r1,r2){ return r1 + r2; } )
  17. )
  18. .val( function(msg){
  19. console.log( msg );
  20. } );

The iterable sequence expresses a sequential series of (sync or async) steps that looks awfully similar to a Promise chain — in other words, it’s much cleaner looking than just plain nested callbacks, but not quite as nice as the yield-based sequential syntax of generators.

But we pass the iterable sequence into ASQ#runner(..), which runs it to completion the same as if it was a generator. The fact that an iterable sequence behaves essentially the same as a generator is notable for a couple of reasons.

First, iterable sequences are kind of a pre-ES6 equivalent to a certain subset of ES6 generators, which means you can either author them directly (to run anywhere), or you can author ES6 generators and transpile/convert them to iterable sequences (or Promise chains for that matter!).

Thinking of an async-run-to-completion generator as just syntactic sugar for a Promise chain is an important recognition of their isomorphic relationship.

Before we move on, we should note that the previous snippet could have been expressed in asynquence as:

  1. ASQ( "http://some.url.1" )
  2. .seq( /*STEP 1*/ request )
  3. .seq( function STEP2(resp){
  4. return ASQ().gate(
  5. request( "http://some.url.2/?v=" + resp ),
  6. request( "http://some.url.3/?v=" + resp )
  7. );
  8. } )
  9. .val( function STEP3(r1,r2){ return r1 + r2; } )
  10. .val( function(msg){
  11. console.log( msg );
  12. } );

Moreover, step 2 could have even been expressed as:

  1. .gate(
  2. function STEP2a(done,resp) {
  3. request( "http://some.url.2/?v=" + resp )
  4. .pipe( done );
  5. },
  6. function STEP2b(done,resp) {
  7. request( "http://some.url.3/?v=" + resp )
  8. .pipe( done );
  9. }
  10. )

So, why would we go to the trouble of expressing our flow control as an iterable sequence in a ASQ#runner(..) step, when it seems like a simpler/flatter asyquence chain does the job well?

Because the iterable sequence form has an important trick up its sleeve that gives us more capability. Read on.

Extending Iterable Sequences

Generators, normal asynquence sequences, and Promise chains, are all eagerly evaluated — whatever flow control is expressed initially is the fixed flow that will be followed.

However, iterable sequences are lazily evaluated, which means that during execution of the iterable sequence, you can extend the sequence with more steps if desired.

Note: You can only append to the end of an iterable sequence, not inject into the middle of the sequence.

Let’s first look at a simpler (synchronous) example of that capability to get familiar with it:

  1. function double(x) {
  2. x *= 2;
  3. // should we keep extending?
  4. if (x < 500) {
  5. isq.then( double );
  6. }
  7. return x;
  8. }
  9. // setup single-step iterable sequence
  10. var isq = ASQ.iterable().then( double );
  11. for (var v = 10, ret;
  12. (ret = isq.next( v )) && !ret.done;
  13. ) {
  14. v = ret.value;
  15. console.log( v );
  16. }

The iterable sequence starts out with only one defined step (isq.then(double)), but the sequence keeps extending itself under certain conditions (x < 500). Both asynquence sequences and Promise chains technically can do something similar, but we’ll see in a little bit why their capability is insufficient.

Though this example is rather trivial and could otherwise be expressed with a while loop in a generator, we’ll consider more sophisticated cases.

For instance, you could examine the response from an Ajax request and if it indicates that more data is needed, you conditionally insert more steps into the iterable sequence to make the additional request(s). Or you could conditionally add a value-formatting step to the end of your Ajax handling.

Consider:

  1. var steps = ASQ.iterable()
  2. .then( function STEP1(token){
  3. var url = token.messages[0].url;
  4. // was an additional formatting step provided?
  5. if (token.messages[0].format) {
  6. steps.then( token.messages[0].format );
  7. }
  8. return request( url );
  9. } )
  10. .then( function STEP2(resp){
  11. // add another Ajax request to the sequence?
  12. if (/x1/.test( resp )) {
  13. steps.then( function STEP5(text){
  14. return request(
  15. "http://some.url.4/?v=" + text
  16. );
  17. } );
  18. }
  19. return ASQ().gate(
  20. request( "http://some.url.2/?v=" + resp ),
  21. request( "http://some.url.3/?v=" + resp )
  22. );
  23. } )
  24. .then( function STEP3(r1,r2){ return r1 + r2; } );

You can see in two different places where we conditionally extend steps with steps.then(..). And to run this steps iterable sequence, we just wire it into our main program flow with an asynquence sequence (called main here) using ASQ#runner(..):

  1. var main = ASQ( {
  2. url: "http://some.url.1",
  3. format: function STEP4(text){
  4. return text.toUpperCase();
  5. }
  6. } )
  7. .runner( steps )
  8. .val( function(msg){
  9. console.log( msg );
  10. } );

Can the flexibility (conditional behavior) of the steps iterable sequence be expressed with a generator? Kind of, but we have to rearrange the logic in a slightly awkward way:

  1. function *steps(token) {
  2. // **STEP 1**
  3. var resp = yield request( token.messages[0].url );
  4. // **STEP 2**
  5. var rvals = yield ASQ().gate(
  6. request( "http://some.url.2/?v=" + resp ),
  7. request( "http://some.url.3/?v=" + resp )
  8. );
  9. // **STEP 3**
  10. var text = rvals[0] + rvals[1];
  11. // **STEP 4**
  12. // was an additional formatting step provided?
  13. if (token.messages[0].format) {
  14. text = yield token.messages[0].format( text );
  15. }
  16. // **STEP 5**
  17. // need another Ajax request added to the sequence?
  18. if (/foobar/.test( resp )) {
  19. text = yield request(
  20. "http://some.url.4/?v=" + text
  21. );
  22. }
  23. return text;
  24. }
  25. // note: `*steps()` can be run by the same `ASQ` sequence
  26. // as `steps` was previously

Setting aside the already identified benefits of the sequential, synchronous-looking syntax of generators (see Chapter 4), the steps logic had to be reordered in the *steps() generator form, to fake the dynamicism of the extendable iterable sequence steps.

What about expressing the functionality with Promises or sequences, though? You can do something like this:

  1. var steps = something( .. )
  2. .then( .. )
  3. .then( function(..){
  4. // ..
  5. // extending the chain, right?
  6. steps = steps.then( .. );
  7. // ..
  8. })
  9. .then( .. );

The problem is subtle but important to grasp. So, consider trying to wire up our steps Promise chain into our main program flow — this time expressed with Promises instead of asynquence:

  1. var main = Promise.resolve( {
  2. url: "http://some.url.1",
  3. format: function STEP4(text){
  4. return text.toUpperCase();
  5. }
  6. } )
  7. .then( function(..){
  8. return steps; // hint!
  9. } )
  10. .val( function(msg){
  11. console.log( msg );
  12. } );

Can you spot the problem now? Look closely!

There’s a race condition for sequence steps ordering. When you return steps, at that moment steps might be the originally defined promise chain, or it might now point to the extended promise chain via the steps = steps.then(..) call, depending on what order things happen.

Here are the two possible outcomes:

  • If steps is still the original promise chain, once it’s later “extended” by steps = steps.then(..), that extended promise on the end of the chain is not considered by the main flow, as it’s already tapped the steps chain. This is the unfortunately limiting eager evaluation.
  • If steps is already the extended promise chain, it works as we expect in that the extended promise is what main taps.

Other than the obvious fact that a race condition is intolerable, the first case is the concern; it illustrates eager evaluation of the promise chain. By contrast, we easily extended the iterable sequence without such issues, because iterable sequences are lazily evaluated.

The more dynamic you need your flow control, the more iterable sequences will shine.

Tip: Check out more information and examples of iterable sequences on the asynquence site (https://github.com/getify/asynquence/blob/master/README.md#iterable-sequences).