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.
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.
Third Line: Declaring constant element1
with a return value of next
method of returnNextElement
object. A new execution context created.
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 2
as 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.
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?
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
.
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
.
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 return
10. That just kicks out this
10as our output of
returnNextElement.next( ). Still in the local memory
newNum` 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 of
createFlow`.
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.
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.