# Generators and Iterables

# Description

# Iterables

Iterable objects are a generalization of arrays. That’s a concept that allows us to make any object usable in a for..of loop.

  • Iterables must implement a method named Symbol.iterator
  • The object result of the call [Symbol.iterator]() is called an iterator
  • An iterator must have the method named next() that returns an object {done: Boolean, value: any}
  • The Symbol.iterator method is called automatically by for..of

See the example hello-symbol-iterator.js (opens new window) in the repo ULL-MII-SYTWS-2021/learning-generators (opens new window). (What will be the output?)

Read the chapter Iterables (opens new window) of JavaScript.info reproducing the examples and exercises.

Interesting

Read the section Name Collisions (opens new window) of the article A Practical Guide to Symbols in JavaScript for an explanation of why is Symbol.iterator a symbol rather than a string.

# Generators

Generators are created by generator functions function* f(…) {…}.

  • Inside generators (only) there exists a yield operator.
  • The outer code and the generator may exchange results via next/yield calls.

See the examples

Read the chapter Generators (opens new window) of JavaScript.info reproducing the examples and exercises.

# Exercise Groups in the book EloquentJS Chapter 6

  1. Write an iterable class called Group that works like the Set JS class (opens new window). Here is a template for the class Group (Exercise Groups (opens new window) in the book EloquentJS Chapter 6):

    class Group {
        constructor() {
            // ... your code 
        }
        add(elt) {
            // ... your code
        }
        delete(elt) {
            // ... your code 
        }
        has(elt) {
            // ... your code
        }
        static from(iterable) {
            // Takes an iterable object as argument and
            // creates a group that contains all the values
            // produced by iterating over it.
        }
        *[Symbol.iterator] () {
            // ... Your code
        }
    }
    
    export { Group };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
  2. Make the Group class from the previous exercise iterable. (Exercise Iterable groups Groups in EloquentJS Chapter 6 (opens new window))

  3. Write the solution as an ES6 module so that can be imported with this syntax:

    #!/usr/bin/env node 
    import { Group } from './eloquent-js-6-2-group-with-generators.js';
    
    let group = Group.from([10, 20]);
    console.log(group.has(10));
    // → true
    console.log(group.has(30));
    // → false
    group.add(10);
    group.delete(10);
    console.log(group.has(10));
    // → false
    
    for (let value of Group.from(['a', 'b', 'c'])) {
      console.log(value);
    }
    // → a
    // → b
    // → c
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    See the Node.js doc Modules: Packages (opens new window) for more details on the use of ECMA 6 Modules in Node.js.

  4. Simplify the solution to making the Group class iterable using a generator instead of a plain iterator as suggested in Chapter 11 of the book Eloquent JS (opens new window)

    Writing iterators is often much easier when you use generator functions. The iterator for the Group class can be written with this generator:

    Group.prototype[Symbol.iterator] = function*() {
        for (let i = 0; i < this.members.length; i++) {
            yield this.members[i];
        }
    };
    
    1
    2
    3
    4
    5

You can see a solution at folder learning-generators/03-using-generators-for-iterables (opens new window) of the repo ULL-MII-SYTWS-2021/learning-generators

# a = yield exp submits exp and receives the b of the next g.next(b)

You have to take into account these facts:

  1. When using a generator g, you can pass one argument (and only one) to next: g.next(a)
  2. The computation was paused just after the evaluation of the last yield expression executed inside g
  3. The call to g.next(a) becomes the result of this last yield expression
  4. The first call generator.next() should be always made without an argument (If passed the argument will be ignored)

I like to see it this way:

  1. when a call to b = g.next(y) is made, the generator is executed until the next a = yield exp expression is reached.
  2. the yield stops after the expression exp has been evaluated and b gets the yielded value exp
  3. but the execution has paused before the assignment to a

    has been made!
  4. The next call to g.next(z) will be set the value returned by the yield as z and thus b will be z

# Exercise one

What is the output of the following code?

function* generator(z) {
    console.log(z); 
    z++;
    let a = yield z+1;
    console.log('Inside generator: '+a); // a is hello
    let b = yield (a+" world!");
    console.log('Inside generator: '+b); // b is 10
    yield b*2;
}

let g = generator(999);
console.log(g.next().value); 
console.log(g.next("hello", "second ignored parameter").value);
console.log(g.next(10).value); 
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Play with the example for different inputs

# Exercise two

What is the output of the following code?

function * gen () {
  const returnedFromYield = yield 'foo'
  yield returnedFromYield
}

const g = gen()

console.log(g.next(1))
console.log(g.next(2))
console.log(g.next(3))
1
2
3
4
5
6
7
8
9
10

# Return in a Generator

A `return` statement in a generator, when executed, will make the generator finish:

  • If a value return v; is returned, it will produce {done: false, value: v}
  • The next call to g.next() will produce {done: true, value: undefined}

What is the output?

function * foo () {
  yield 123
}

function * bar () {
  return yield 123
}

const f = foo()
const b = bar()

console.log(
  f.next(),
  f.next(),
  
  b.next(),
  b.next()
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Much like a return statement, an error thrown inside the generator will make the generator finished — unless caught within the generator's body.

# The yield* directive delegates the execution to another generator

The yield* directive delegates the execution to another generator.

yield* anotherGen iterates over the generator anotherGen and forwards its yields outside, as if the values were yielded by the outer generator.

The result is the same as if we inlined the code from nested generators into the outer generator.

See the example hello-composition.js (opens new window)

# Delivery

Read both chapters and delivery a report like the one in ULL-MII-SYTWS-2021/learning-generators (opens new window)

# See

Grading Rubric#

Comments#

Last Updated: 3 months ago