JavaScript the Fun Part


modern-javascript

generators-and-promises


While promises are a little bit confusing, at least at first, and generators are more, how much does it for combination of both?

We will review it, but you should know that the pattern is more complex and at the same time powerful and because of these JS community decided to wrap these powerful pattern is a more friendly style which finally became async-await.

co-routine

In the previous post we saw an example of using a simple async function, here is the generator function part:

function* generatorF(){
    log( "start of generatorF()" );
    const a = yield asyncF( 1 );
    const b = yield asyncF( 2 );
    const c = yield asyncF( 3 );
    const d = yield asyncF( 4 );
    const e = yield asyncF( 5 );
    const r = a + b + c + d + e;
    log( "end of generatorF(), sum:", r );
    return "it is ready: " + r;
}

and we simulate the forwarding part of its iterator in this way:

function asyncF( input ){
    const deley = Math.round( Math.random() * 1000 );
    setTimeout(function(){
        log( `asyncF( ${input} ) ...`);
        const step = iterator.next( input );
        log( "step:", step );
    }, deley );

    return "not ready yet!";
}

So when we run the (= start) the generator function in this way:

var iterator = generatorF();
iterator.next();

Then by calling asyncF( number ) the next step happens inside the function and we can forward the states.

With promises we almost do the same except that instead of dealing with a simple data like a number, we are facing a more complex kind of data which is the promises method and also the iterator part of our generator function..

By now you should see that cycle happens here. A communication or a kind of synchronization between our generator function and out iterator.

And for this pattern we have libraries to do this part for us, so we do not be forced to code it manually; e.g:

Example 1

Here we just double the number, that is it.

const log = console.log.bind( console );
log( "... start of main ..." );

function* generatorF(){
    log( "start of generatorF() ..." );
    
    const y1 = yield promiseF( 1 );
    log( "y1", y1 );
    
    const y2 = yield promiseF( 2 );
    log( "y2", y2 );
    
    const y3 = yield promiseF( 3 );
    log( 'y3', y3 );

    const y4 = yield promiseF( 4 );
    log( 'y4', y4 );

    log( "end of generatorF() ..." );
};

// create the iterator part
const iterator = generatorF();

// trigger the generator (= start to run)
iterator.next();

// a wrapper around a promise
function promiseF( input ){
     return new Promise(function( resolve, reject ){

        // go to event queue (= later execution)
        setTimeout(function(){
            const output = input + input;
            // for forwarding the state inside generator 
            // also returning back the data we wanted
            function run( data ){
                // step is an object: { value: here-will-be-a-promise, done: boolean }
                const step = iterator.next( output );
                if( step.done ){
                    return resolve( output );
                 }

                 // step.value is a promise
                 step.value
                    .then( run )
                    .catch( iterator.throw )
            }
            // start the run() with input e.g. { tech: 'js' }
            run( input )
        }, 500 );
    })
}

log( ".... end of main...." );

the output

... start of main ...
start of generatorF() ...
.... end of main....
y1 2
y2 4
y3 6
y4 8
end of generatorF() ...

Example 2

Here we have a dummy database and look for a match and return an output

const log = console.log.bind( console );
log( "... start of main ..." );

function* generatorF(){
    log( "start of generatorF() ..." );
    
    const y1 = yield promiseF( { tech: 'js' } );
    log( "y1", y1 );
    
    const y2 = yield promiseF( { tech: 'css' } );
    log( "y2", y2 );
    
    const y3 = yield promiseF( { tech: 'html' } );
    log( 'y3', y3 );

    const y4 = yield promiseF( { tech: 'js' } );
    log( 'y4', y4 );

    log( "end of generatorF() ..." );
};

// create the iterator part
const iterator = generatorF();

// trigger the generator (= start to run)
iterator.next();

// dummy database
const database = {
    "js": "Programming Language",
    "css": "Cascading Style Sheet",
    "html": "Hyper Text Markup Language"

};

// a wrapper around a promise
function promiseF( input ){
     return new Promise(function( resolve, reject ){
        
        // simulate random delay
        const deley = Math.round( Math.random() * 1000 );

        // go to event queue (= later execution)
        setTimeout(function(){

            // query from database
            const output = database[ input.tech ];

            // for forwarding the state inside generator 
            // also returning back the data we wanted
            function run( data ){
                // step is an object: { value: here-will-be-a-promise, done: boolean }
                const step = iterator.next( output );
                if( step.done ){
                    return resolve( output );
                 }

                 // step.value is a promise
                 step.value
                    .then( run )
                    .catch( iterator.throw )
            }
            // start the run() with input e.g. { tech: 'js' }
            run( input )
        }, deley );
    })
}

log( ".... end of main...." );

output

... start of main ...
start of generatorF() ...
.... end of main....
y1 Programming Language
y2 Cascading Style Sheet
y3 Hyper Text Markup Language
y4 JavaScript Language
end of generatorF() ...

Example 3

Here instead of firing some promise one by one, we trigger all of them at same time.

But our generator function will pause for them in different order!

const log = console.log.bind( console );
log( "... start of main ..." );

function* generatorF(){
    log( "start of generatorF() ..." );
    
    const p1 = promiseF( { tech: 'js'} );
    const p2 = promiseF( { tech: 'css'} );
    const p3 = promiseF( { tech: 'html' } );
    
    log( "start p1 ..." );
    log( "start p2 ..." );
    log( "start p3 ..." );

    const html = yield p3;
    log( "html:", html );
    
    const js = yield p1;
    log( "js:", js );
    
    const css = yield p2;
    log( 'css:', css );

    log( "end of generatorF() ..." );
};

// create the iterator part
const iterator = generatorF();

// trigger the generator (= start to run)
let step = iterator.next();

// dummy database
const database = {
    "js": "Programming Language",
    "css": "Cascading Style Sheet",
    "html": "Hyper Text Markup Language"
};

// a wrapper around a promise
function promiseF( input ){
     log( "input:", input );
     return new Promise(function( resolve, reject ){
        
        // simulate random delay
        const deley = Math.round( Math.random() * 1000 );

        // go to event queue (= later execution)
        setTimeout(function(){

            // query from database
            const output = database[ input.tech ];

            // for forwarding the state inside generator 
            // also returning back the data we wanted
            function run( data ){
                // step is an object: { value: here-will-be-a-promise, done: boolean }
                const step = iterator.next( output );
                if( step.done ){
                    return resolve( output );
                 }

                 // step.value is a promise
                 step.value
                    .then( run )
                    .catch( iterator.throw )
            }
            // start the run() with input e.g. { tech: 'js' }
            run( input )
        }, deley );
    })
}

log( ".... end of main...." );

output

... start of main ...
start of generatorF() ...
input: { tech: 'js' }
input: { tech: 'css' }
input: { tech: 'html' }
start p1 ...
start p2 ...
start p3 ...
.... end of main....
html: Hyper Text Markup Language
js: Cascading Style Sheet
css: Programming Language
end of generatorF() ...

Faster and more Readable code

You saw that while we can start our promises at the same time, but we can synchronize them in the order we want; plus it is more readable.

Just notice out logic happens inside generator function, in fact the flow of our programming happens inside generator function.

function* generatorF(){
    log( "start of generatorF() ..." );
    
    const p1 = promiseF( { tech: 'js'} );
    const p2 = promiseF( { tech: 'css'} );
    const p3 = promiseF( { tech: 'html' } );
    
    log( "start p1 ..." );
    log( "start p2 ..." );
    log( "start p3 ..." );

    const html = yield p3;
    log( "html:", html );
    
    const js = yield p1;
    log( "js:", js );
    
    const css = yield p2;
    log( 'css:', css );

    log( "end of generatorF() ..." );
};

Problem with this pattern

Did you notice that the output is wrong!

html: Hyper Text Markup Language
js: Cascading Style Sheet
css: Programming Language

Why? Because they all run at the same time, but the one that is finished first, comes first. The iterator has forwarded inside that function and gives us a wrong output

Which means we have to code more to synchronize even further.

Solution?

We can use async-await or libraries that do this for us.

An example using async-await

const log = console.log.bind( console );
log( "... start of main ..." );

async function asyncF(){
    log( "start of asyncF() ..." );
    
    const p1 = promiseF( { tech: 'js'} );
    const p2 = promiseF( { tech: 'css'} );
    const p3 = promiseF( { tech: 'html' } );
    
    log( "start p1 ..." );
    log( "start p2 ..." );
    log( "start p3 ..." );

    const html = await p3;
    log( "html:", html );
    
    const js = await p1;
    log( "js:", js );
    
    const css = await p2;
    log( 'css:', css );

    log( "end of asyncF() ..." );
};

asyncF();

// dummy database
const database = {
    "js": "Programming Language",
    "css": "Cascading Style Sheet",
    "html": "Hyper Text Markup Language"
};

// a wrapper around a promise
function promiseF( input ){
     return new Promise(function( resolve, reject ){
        // simulate random delay
        const deley = Math.round( Math.random() * 1000 );
        // go to event queue (= later execution)
        setTimeout(function(){
             log( "deley:", deley, "for", input );
             const output = database[ input.tech ];
             resolve( output );
        }, deley );
    })
}

log( ".... end of main...." );

and the output. The order is correct event they have different delay time.

For example the css delay time is 307, which means it is finished first, but we get html first and not css.

... start of main ...
start of asyncF() ...
start p1 ...
start p2 ...
start p3 ...
.... end of main....
deley: 307 for { tech: 'css' }
deley: 502 for { tech: 'js' }
deley: 948 for { tech: 'html' }
html: Hyper Text Markup Language
js: Programming Language
css: Cascading Style Sheet
end of asyncF() ...

What to do next?

It is more complex than what you see here to deal with generators and promises together, and because of that there are libraries to simply this pattern for us. You try them.

More

There are a lot of things about this pattern. Here is some resources that start with.


Update: Tue Oct 08 2019 12:45:37 GMT+0330 (Iran Standard Time)