Deep dive into JavaScript Generator Functions & Generators

Tags: javascript, functional, webdev, tutorial
Published: 2021-06-13 01:07:19

- In JavaScript, when a function is called, it executes the code within its body until it reaches a return statement (if a function doesn't have a return statement, it returns undefined). We can say that we have no control over the flow of the functions. which means, we cannot make a function make multiple return at different stepped calls, or make the function's code stop after a certain block of code...

- Contrary to the function's flow of control. There's something called generators in JavaScript that can yield (return) multiple times, one after another.

- To create a generator, we need a **generator function** which is defined with the ***function**** syntax, as follow:

```javascript
// Defining our function generator
function* fnGenerator() {
    yield 1;
    yield 2;
    yield 3;
    return 4;
}

// Create a generator instance from the generator function
const generator = fnGenerator();

console.log(generator); // Iterator [Generator] {}
```

The generator instance has 3 methods:
- next()
- return()
- throw()

### The Next() method
- It returns an object that represents the state of our generator.
- The returned object has 2 properties: **done** and **value**.
- The **value** property contains the latest yield value, when the next() method is called. if there's no value in a yield statement, then it yields ```undefined``` by default.
- The **done** property which is a boolean, refers to whether we've iterated through all the sequences of our iterable. When the next() methods returns the value of the ```return``` statement in our Function Generator, by default the generator instance's done because ```true``` and we can no longer iterate through it, unless we initialize a new generator instance or reset it (we'll see how to do that later).
- It runs the code of the Function Generator until it reaches the nearest yield statement ```yield value_to_yield```.
- After reaching the **yield** the code execution pauses there until the next call for **next** of **return**.

* Example

```javascript
// Defining our function generator
function* fnGenerator() {
    yield 1;
    yield 2;
    yield 3;
    return 4;
}

// Create a generator instance from the generator function
const generator = fnGenerator();

console.log(generator.next()) // { value: 1, done: false }
console.log(generator.next()) // { value: 2, done: false }
console.log(generator.next()) // { value: 3, done: false }
console.log(generator.next()) // { value: 4, done: true }

// Now that our generator is done, what if we call next again?
console.log(generator.next()) // { value: undefined, done: true }
console.log(generator.next()) // { value: undefined, done: true }
```

- After our generator's done is true, we say that it's complete and we cannot reuse it or reset. If we'd still need one, we have to initialize a new instance.

### The Return() method
- It returns the given value to the generator and finishes the generator (sets its done property to true).
- It takes an optional argument. It updates the the **value** property of the generator's returned / yielded value (Example II). If no parameter is passed, then the **value** becomes undefined (Example I).

* Example I

```javascript
function* gen() {
  yield 1;
  yield 2;
  yield 3;
  return 4;
}

const g = gen();
g.next() // { value: 1, done: false }
g.return() // { value: undefined, done: true }
```

* Example II

```javascript
function* gen() {
  yield 1;
  yield 2;
  yield 3;
  return 4;
}

const g = gen();
g.next() // { value: 1, done: false }
g.return(9999) // { value: 9999, done: true }
```

### The throw() method
- It allows us to throw an error into a generator.
- It returns the same object as the **next** and the **return** methods.
- You pass to it the exception to throw ```throw(exception)```.
- You use this method if you're handling errors in your Function Generator.
- If you're not familiar with error handling in JavaScript, check [this documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch).
- This method isn't used as much as the **next** method. Check [this example](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/throw#examples)

## Iterating through a generator
- You can go through a generator's yielded values by using the **next()** methods as many times as possible until it's done and you can also loop through them (the value property and not the entire object that represents the state of the generator) with the ``` for ... of ``` as follow:

```javascript
function* fnGenerator() {
    yield 1;
    yield 2;
    yield 3;
    return 4;
}

let iterator = fnGenerator();

for (let v of iterator) {
  console.log(v);
}
/*
output will be:
1
2
3
*/
```

- If you want to loop through it and get the whole state (returned object that represents the yielded value), you can do according to the done value (as long as done isn't true a.k.a as long as it's not completed yet), as follow:

```javascript
function* fnGenerator() {
    yield 1;
    yield 2;
    yield 3;
    return 4;
}

let iterator = fnGenerator();

let next;

while (!(next = iterator.next()).done) {
   console.log("Current state = ", next)
}

/*
output will be:
Current state =  {value: 1, done: false}
Current state =  {value: 2, done: false}
Current state =  {value: 3, done: false}
*/
```

## Reset generator's state
- You can reset the generator's yielded value to its initial value or update by passing an argument to the **next** method.
- The **next** method takes an optional parameter. The passed value to  the **next** method will be assigned as a result of a yield statement.

* Example

- Our function generator

```javascript
function* dumpGen() {
  let count = 0;
  while (count < 3) {
    let reset = yield count += 1;
    if (reset === 0) {
      count = 0;
    }
  }
}
```

- Our first iterator without state reset

```javascript
let iterator1 = dumpGen();

iterator1.next();
iterator1.next();
iterator1.next();
iterator1.next();

/*
output will be:
{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: undefined, done: true }
*/
```

- As you can see in the example with the ```iterator1``` above, we haven't passed anything to the next method to update or reset the generator's state. Now let's see what's going to happen when we pass 0.

```javascript
let iterator2 = dumpGen();

iterator2.next();
iterator2.next();
iterator2.next();
iterator2.next(0);
iterator2.next();

/*
output will be:
{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: 1, done: false }
{ value: 2, done: false }
*/
```

- In the 2nd example with ```iterator2```, when we passed the 0 argument to the next, the generator got back to its initial state. What happened is that when we passed 0 to the **next** method, we assigned it to the reset variable. But we did that before the generator is complete (done === true).

- If we try to reset the state after done has become true, nothing will change, because once a generator is complete, you cannot reset it:

```javascript
let iterator2 = dumpGen();

iterator2.next();
iterator2.next();
iterator2.next();
iterator2.next();
iterator2.next(0);
iterator2.next();

/*
{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: undefined, done: true }
{ value: undefined, done: true }
{ value: undefined, done: true }
*/
```

## Iterate through a generator's arguments

- You can pass to the generator as many arguments as you wish.
- You can loop through those arguments in 3 different ways

### 1st way

```javascript
function *dumpGen() {
  for (let arg of arguments) {
    yield arg;
  }
}

let iterator = dumpGen(1,2,3,4,5);

iterator.next();
iterator.next();
iterator.next();
iterator.next();
iterator.next();
iterator.next();

/*
output will be:
{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: 4, done: false }
{ value: 5, done: false }
{ value: undefined, done: true }
*/
```

### 2nd way

```javascript
function *dumpGen() {
  for (let i = 0; i < arguments.length; i++) {
    yield arguments[i];
  }
}

let iterator = dumpGen(1,2,3,4,5);

iterator.next();
iterator.next();
iterator.next();
iterator.next();
iterator.next();
iterator.next();
 
/*
output will be:
{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: 4, done: false }
{ value: 5, done: false }
{ value: undefined, done: true }
```

### 3rd way

```javascript
function *dumpGen() {
  yield* arguments
}

let iterator = dumpGen(1,2,3,4,5);

iterator.next();
iterator.next();
iterator.next();
iterator.next();
iterator.next();
iterator.next();

/*
output will be:
{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: 4, done: false }
{ value: 5, done: false }
{ value: undefined, done: true }
*/
```

- In the 3rd way, the ```yield* ``` mimics the for loop to go through the arguments.

## Deconstruction

- You can deconstruct and get a generator's yielded values at once, as follow:

```javascript
function *dumpGen() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
}

let arr = [...dumpGen()];

console.log(arr); // [1, 2, 3, 4]
```

## Conclusion
- Function generators return a Generator object that behaves as iterator.
- We use ```yield``` to pause a function's flow to produce an undone state with a certain value.
- An iterator is mostly used with the ```next()``` method to get the most recent yielded state.
- if you want to read more about Generators, check the following resources:
- [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator)
- [javascript.info](https://javascript.info/generators)