Iterators and Generators in JavaScript (Part - 2)

Iterators and Generators in JavaScript (Part - 2)

·

7 min read

Once we start thinking of our data as flows (where we can pick of an element one by one) we rethink how we produce those flows. Javascript now lets us produce the flows using a function to set what individual element will be returned next. We need to understand that the built-in function that produces flows does not call a function directly to return the next element. They instead have an object method 'next', that when called gives us the next element. We will build that from scratch.

function createFlow(array){
  let i = 0;
  const inner = {
         next: function( ){
         const element = array[ i ]
         i++
  }
  } 
  return inner
}
const returnNextElement = createFlow( [ 1, 2, 3 ] )
const element1 = returnNextElement.next( )
const element2 = returnNextElement.next( )

JavaScript built-in returns next elements, they are called `iterators'. They actually object with a next method that when called returns our next element from the stream or flow. So we don't return its element instead, we call the method on it.

Let's restructure the above code slightly to make sure we are truly clear on what this is doing.

First Line: Creating a function createFlow.

Second Line: Declaring a const returnNextElement which is undefined right now, it will be the return value of createFlow. Calling createFlow creates a new execution context.

Screenshot 2021-07-08 at 18-16-46 Figma.png

The first thing in the memory that will be going to store is array which is an argument. The array has values of 1, 2, 3. Next, we store i with 0 value. We are not going to define the function independently and return it out. instead, we are going to define the function as a method on an object. That object called inside the createFlow is inner. It has a property next that stored a function definition.

The function that is assigned to next gets the bond to all surrounding data as soon as it is defined through the [[ scope ]] property. Now we are returning out of the object that was assigned to inner and then it stores in the returnNextElement in global memory.

Screenshot 2021-07-08 at 17-46-38 Figma.png

Third Line: Declaring constant element1 with a return value of next method of returnNextElement object. A new execution context created.

Screenshot 2021-07-08 at 19-45-41 Figma.png

Let's see what is happening inside the execution context. First a constant element is declared in memory with array[ i ]. We try to find array[ i ] but we could not get it, then we move to function backpack or closure to check the value and we get it there. In the next line, i is increasing by one value. Finally, we return the element value which is 1, and store it in element1.

Fourth Line: In the same way element2 will get a return value as 2as we have seen in the third line.

The reason we did it was to recognize the design of built-in iterators, they actually object with a function on them, that's going to do that stuff. Built-in iterators actually produce element that gets returned out, not as a number. Instead, produce out an object with a value and another property called done. Which is false until we have called returnNextElement.next( ) again. This time value is 2, still done is false. Again next time value is 3, still done is false. Again next time value is undefined, done is true now.

We are now going to start using built-in tools to give us these returnNextElement.next functions that when run will give us each of our elements one by one. And we are going to produce those flows of data, not from underlying collections 1, 2, 3. Instead, we are going to produce these flows using function. We are actually going to define a function that has a kind of intermediate return.

function *createFlow( ){
 yeild 1
 yeild 2
 yeild 3
}
const returnNextElement = createFlow( )
const element1 = returnNextElement.next( )
const element2 = returnNextElement.next( )

The above code uses the star function which is a generator function to produce our flows of data. Let's go line by line.

First Line: Declaring a generator function createFlow.

Second Line: Define a constant returnNextElement which is going to be the output of createFlow function. It does not go inside createFlows execution context. Instead, it returns out a special generator object with a next function on it.

Screenshot 2021-07-08 at 19-32-35 Figma.png

We have now finished the call to createFlow.

Third Line: Defining a constant element1 that store the return value of returnNextElement.next( ) function execution. What returnNextElement.next( ) going to do?

think.gif

It executes createFlow and opens the execution context of createFlow. The function which is assigned to the next property has an intimate bond when it was born at createFlow. When we call the next it's going to start initiate calling createFlow the function from which it was born. When it does that , it's going into the createFlow.

Screenshot 2021-07-08 at 19-57-18 Figma.png yield is a super powerful keyword just like a light return that exits out of the function. But it's suspending the execution context, it's not ending it. In this line, we are going to grab that 4 and we are going to yield it out as the output of our returnNextElement.next call which is going to assign to element1.

Fourth Line: Declaring element2 and in the same way it assigned with yielded value 5.

We now get to produce our flows using a function. What that allows us to do is dynamically set what data flows out to us when we turn on the tap and give ourselves the next element.

function *createFlow( ){
  const num = 10
  const newNum = yeild num
  yeild 5 + newNum
  yeild 6
}
const returnNextElement = createFlow( )
const element1 = returnNetElement.next( )
const element2 = retunNextElement.next(2)

First Line: Defining createFlow as a generator function.

Second Line: Defining constant returnNextElement is assigned with generator object with property next that assigned with a function.

Third Line: Defining a constant element1 which assign with the return value returnNextElement.next function call. When we call that function it opens the execution context of createFlow.

Screenshot 2021-07-08 at 21-07-31 Figma.png

Inside createFlow execution context we are defining constant num and assigned with value 10. Next, we are defining a newNum as constant. When we execute the righthand side of 'newNumwe don't have a chance to evaluate a value. Because it's like seeing return10. That just kicks out this10as our output ofreturnNextElement.next( ). Still in the local memorynewNum` is undefined.

Fourth Line: Define elemnet2 as constant and is going to be a returned value of returnNextxElement.next( 2 ). It going to take us back into the execution context ofcreateFlow`.

Screenshot 2021-07-08 at 21-56-19 Figma.png

Where did we leave in the previous execution? We left being rapidly kicked out of our function and never getting a chance to assigning to newNum. When we come back in, whatever we pass in as our input to next that takes us back into createFlow is going to be the evaluated result of this last right-hand side work. This is going to allow us to pass data back into our execution context, almost like an argument back into it. And that's the very nature of the design of these generator functions. That when you go back into them, you get to insert data back into their local execution context as the evaluated result of the previous yield expression. We are passing value 2 which store in newNum. In the next line yield 5 + neNum which is yield 7 then kicks out7 and is stored in element2.

Look how much dynamic control we have over the function that gives us the next element of our flow of data. Now the question is how the execution context stored? We are therefore storing on the next function. We are storing execution context before we return back into it, to resume it. It's not staying on the call stack. We hold onto it with two pieces of information. One of our backpacks of data contained with num which is 10 and newNum which is 2. The other one contains the position in the generator function. The line number and position of the code which is stored in squared brackets.

Screenshot 2021-07-08 at 22-46-40 Figma.png

That's all our execution context when it is not running. When you start running it, you take the thread to let that line fall and you make sure that your local data.

Did you find this article valuable?

Support Utpal Pati by becoming a sponsor. Any amount is appreciated!