Saturday, 15 August 2020

Generators in javascript

 Normal functions vs Generator functions :


functions in javascript "run until return/end".

Generator Functions " run until yield/return/end".


Generator functions :

======================


A generator is a function that can stop midway and then continue from where it stopped.


Generator functions once called,returns the 'Generator Object',which holds the 

entire 'Generator Iterable' that can be iterated using next() method or for..of loop.


" Every next() call on the generator executes every line of code until the next yield it encounters and suspends its execution temporarily".


Syntactically generators are identified with a *, either 'function* X' or 'function *X' -- both mean the same thing.




Note :

Once created, calling the generator function returns the Generator Object. 

This generator object needs to be assigned to a variable to keep track of the subsequent next() methods called on itself.

If the generator is not assigned to a variable then it will always yield only till first yield expression on every next().


The generator functions are normally built using yield expressions. 

Each yield inside the generator function is a stopping point before the next execution cycle starts. 

Each execution cycle is triggered by means of next() method on the generator.

On each next() call, the yield expression returns its value in the form of an object containing the following parameters.


{ value: 10, done: false } // assuming that 10 is the value of yield


Value — is everything that is written on the right side of the yield keyword, it can be a function call, 

object or practically anything. For empty yields this value is undefined.


Done — indicates the status of the generator, whether it can be executed further or not. 

When done returns true, it means that the function has finished its run.


1.Assign the generator to a variable.



ex : function* generatorFunction(i){

   yield i;

   yield i+1;   

}


let generator = generatorFunction(5);

console.log(generator.next()); //{value: 5, done: false}

console.log(generator.next()); //{value: 6, done: false}


//Accessing generatorFunction directly


console.log(generatorFunction(5).next()); //{value: 5, done: false}

console.log(generatorFunction(5).next()); //{value: 5, done: false}


the generator function accessed directly without a wrapper always executes only until the first yield.

Hence , by definition you need to assign the Generator to a variable to properly iterate over it.


Lifecycle of a Generator Function 


Each time a yield encountered the generator function returns an object containing the value of the 

encountered yield and the done status.


Similarly, when a return is encountered, we get the return value and also done status as true.

Whenever, done status is returned as true,is essentially means that the generator function has 

completed its run, and no further yield is possible.


Note: Everything after the first 'return' is ignored,including other 'yield' expressions.




2.Assigning Yield to a Variables


ex : 

function *generatorFunction(){

  const x=yield 1;

  console.log(x);

  const y=yield 2;

  console.log(y);

}


let generator=generatorFunction();


console.log(generator.next());

//{value: 1, done: false}

console.log(generator.next());

// {value: 1, done: false}, x undefined

console.log(generator.next());

//{value: undefined, done: true}, y undefined


Note : 

Starting from second next(), the previous yield is replaced with arguments passed in the next function. 

Since, we do not pass anything here in the next method, its assumed that the entire ‘previous-yield expression’ as undefined.


3.Passing Arguments to the next() method :


ex : 

  function *generatorFunction(i){

     console.log(i);

const j = 5 * (yield (i * 10));

console.log(j);

const k = yield (2 * j / 4);

console.log(k);

return (i + j + k);

  }

  

  var generator1 = generatorFunction(10);

   // i is 10

  console.log( generator1.next(20));

  // {value: 100, done: false}

  console.log( generator1.next(10));

  // j is 50

  // {value: 25, done: false}

  console.log( generator1.next(5));

  // k is 5

  // {value: 65, done: true}

  

4. Passing Yield as an Argument of a function


 ex : 

  function *generatorFunction(){

    yield;      // pause here and returned undefined

foo( yield "I am useless"); // pause and wait for next() to finish. 

  } 

  

  function foo(x){

    console.log(" Just printing argument passed ", x);

  }

  

  let generator3 = generatorFunction();

  console.log( generator3.next());

  // {value: undefined, done: false}

  console.log( generator3.next());

  // {value: "I am useless", done: false}

  console.log( generator3.next());

  // Just printing argument passed  undefined

  // {value: undefined, done: true}

  

5. Yield with a Function Call


Apart from returning values yield can also call functions and return the value or print the same.

 

 function *fetchUser(){

   const user = yield getData();

   console.log(user) // undefined

 }

 

 function getData(){

   return { name:"Jim Cooper", dob:'1991'};

 }

  

 let fetchGen = fetchUser();

 console.log(fetchGen.next());

// { value: {name: "Jim Cooper", dob: "1991"} , done: false}

  console.log(fetchGen.next());

// undefined

//{value: undefined, done: true}


6. Yield with Promises


Yield with promises follows the same approach as the function call,

instead of returning a value from the function, it returns a promise 

which can be evaluated further for success or failure.


ex :


function * fetchUser(action){

  const user = yield apiCall();

}


function apiCall(){

  return new Promise(resolve => {

    setTimeout(() => {

  resolve ( { name:'Jim Cooper', dob:'1991'});

},2000);

  })

}


let fetchgen2 = fetchUser();


// prints user data after 2 seconds

console.log( fetchgen2.next().value.then( n => console.log(n)));

// Promise

// { name: "Jim Cooper", dob:"1991"}


The apicall returns the promises as the yield value, when resolved after 2 seconds prints the value we need.


7. Yield*

Yield* when used inside a generator function delegates another generator function.


function* g1(){

 yield 2;

 yield 3;

 yield 4;

}


function* g2(){

 yield 1;

 yield* g1();

 yield 5;


}


let iterator = g2();


console.log( iterator.next()); // {value: 1, done: false}

console.log( iterator.next()); // {value: 2, done: false}

console.log( iterator.next()); // {value: 3, done: false}

console.log( iterator.next()); // {value: 4, done: false}

console.log( iterator.next()); // {value: 5, done: false}

console.log( iterator.next()); // {value: undefined, done: true}


8. Yield* with Return

 Yield* with a return behaves a bit differently than the normal yield*.

 When yield* is used with a return statement it evaluates to that value,

 meaning the entire yield* function() becomes equal to the value returned 

 from the associated generator function.

 

 

ex : 


  function* genFuncChild(){

    yield 1;

yield 2;

return 'foo';

yield 3;     // note that this is ignored

  }

  

  function* genFuncMain(){

   const result = yield* genFuncChild();

   console.log(result);

   yield 'the end';

  }

 

 let generator = genFunction();

 console.log(generator.next());  // yield 1

 console.log(generator.next());  // yield 2

 console.log(generator.next());  //  returns 'foo' and yields 'the end'

 console.log(generator.next());  // finishes run

 

9. Yield* with a Built-in iterable Object


yield* can also iterate over iterable objects like Array,String and Map.


function* genFunc(){

 yield* [1,2];

 yield* 'HI';

 yield* arguments;


}


let generator = genFunc(5,6);


console.log(generator.next());  // prints 1

console.log(generator.next());  // prints 2

console.log(generator.next());  // prints H

console.log(generator.next());  // prints I

console.log(generator.next());  // prints 5

console.log(generator.next());  // prints 6


yield* iterates over every possible iterable object that is 

passed as its expression.


10.yield with for..of


Every Iterator/generator can be iterated over a for..of loop.

similar to our next() method which is called explicitly , for..of loop 

internally moves on to the next iteration based on the 'yield keyword'.

And it iterates only till the 'last yield' and doesn't process the return 

statement like next() method.


ex : 

   function* genFuncChild(){

     yield 1;

yield 2;

return 'foo';

yield 3;  // note that this is ignored

   }

   

   function* genFuncMain(){

   const result = yield* genFuncChild();

   console.log(result);

   yield 'the end';

  }

 

 let generatorforof=genFuncMain();

 

 // using for..of to print the generator

 for(let i of generatorforof){

   console.log(i);

 }

//result 

//  1

//  2

//  the end


The final return in not printed, because for..of loop iterates only till the last yield.

So,it comes under best practice to avoid return statements inside a generator function as 

it would affect the reusability of the function when iterated over a for..of.


No comments:

Post a Comment