Skip to content

05.1. Generators e Iterators

Carlos Jaramillo edited this page Jun 6, 2021 · 3 revisions

Generadores e Iteradores

Colecciones (Arreglos), también podemos trabajar con "colecciónes" con iteradores y generadores. Es relativo, porque los iteradores y generadores pueden iterar sobre un rango de datos sin la necesidad de una colección. La clara ventaja es rendimiento, porque cuando usamos un arreglo en JS, es necesario almacenar el arreglo completo en la memoria virtual (es trivial con arreglos pequeños), si quisieramos iterar de 0 a infinito sería imposible con arreglos porque la memoria virtual tiene un limite. Aqui entran los iteradores y generadores con los cuales podemos desarrollar nuestros propios mecanismos de iteración sin tener que alojar toda la colección en la memoria virtual, trabajando con un dato a la vez. Adicionalmente estas nuevas estructuras nos permiten implementar el concepto de iteración sobre objetos personalizados.

Iterator

De acuerdo al portal de dev de mozilla, un Iterador es cualquier objeto que implementa el Iterator protocol, es decir cualquier objeto que implemente un metodo next, que retorne un objeto con una propiedad value y una propiedad done, implementa el protocolo y por lo tanto es un iterador

Caracteristicas:

  • Aun que la intención es iterar una colección de valores, los valores se producen 1 a la vez. No hay arreglos ni estructuras que almacenen todos los datos, lo que beneficia el espacio en memoria. Cada valor que se itera se produce cuando llamamos next
  • La interacción es lazy, cuando recorremos con un ciclo un arreglo, todos los elementos se imprimen tan pronto como es posible para la PC, con un iterador podríamos llamar el siguiente elemento cuando queramos o no hacerlo.
  • Los iteradores no tienen forma de reiniciarlos, sólo se recorren una vez. Cada llamada despues de que el iterador haya terminado de recorrerse debe retornar el valor final del iterador, y la propiedad done: true
  • Podemos recorrer un iterador con un ciclo
let iterador = {}; // empty object
// this object implements iterator protocol, so right now is an iterator
let iterador = {
  next(){
    return { 
      value: null,
      done: true,
    }
  }
}; 
// iterator working
let iterador = {
  currentValue: 1,
  next(){
    let result = { value: null, done: false};
    if (this.currentValue > 0 && this.currentValue <= 5) {
      result = { value: this.currentValue, done: false };
      this.currentValue += 1;
    } else {
      result = { done: true };
    }
    return result;
  }
}; 


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


let item = iterador.next();

while(!item.done) {
  console.log(item.value);
  item = iterador.next();
}

Generadores (Generators)

Son usados por las librerías co, redux-sagas, y más. Es muy similar a un iterador, tiene algunos beneficios como la sintaxis, porque en un generador no tienes que hacerte cargo del estado del objeto. Algunos los definen como funciones, que pueden ser detenidas en su ejecución para luego ser reanudadas en el punto en el que se quedaron.

function* counter(){
  console.log('estoy aqui');
  yield 1;
  console.log('ahora estoy aqui');
  yield 2;
}

let generator = counter();

console.log(generator.next()); // print first console log, and { done: false, value: 1 }
console.log(generator.next()); // print second console log, and { done: false, value: 2 }

console.log(generator.next()); // { done: true, value: undefined }
// the same example as a iterator, but with generators
function* counter(){
  for(var i = 1; i <= 5; i++) {
    yield i;
  }
}
let generator = counter();

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

Yield es muy similar a return, porque yield produce valores para un generador. Aunque no mandamos a llamar return desde la función generador el lo hace de manera implicita.

  • Si mandamos a llamar return con un valor en una función generadora, será igual a yield pero poniendo el done en true. Es decir, sólo puede ser llamado 1 vez.
function* retornador(){
  return 3;

  // if we add yield here never will be returned;
  yield 5;
}
let g = retornador();
console.log(g.next()); // { done: true, value: 3 }
console.log(g.next()); // { done: true, value: undefined }

Son funciones especiales, pueden pausar su ejecución y luego volver al punto donde se quedaron recordando su scope.

  • Los generadores regresan una función.
  • Empiezan suspendidos y se tiene que llamar next para que ejecuten.
  • Regresan un value y un boolean done que define si ya terminaron.
  • yield es la instrucción que regresa un valor cada vez que llamamos a next y detiene la ejecución del generador.
function* simpleGenerator() {
  console.log("GENERATOR START");
  yield 1; 
  yield 2; 
  yield 3;
  console.log("GENERATOR END");
}

const gen = simpleGenerator();
gen.next();
// GENERATOR START
// {value: 1, done: false}
gen.next();
// {value: 2, done: false}
gen.next();
// {value: 3, done: false}
gen.next();
// GENERATOR END

Se prestan para crear funciones eficientes en memoria, ejemplo la secuencia fibonacci, una función que imprime la secuencia, que lo que hace es sumar los dos número anteriores para generar uno nuevo.

function* fibonacci() {
  let a = 1, b = 1;
  while (true) {
    const nextNumber = a + b;
    a = b;
    b = nextNumber;
    yield nextNumber;
  }
}

Delegación de Generadores

Yield, se manda a llamar junto a una expresión que produce un resultado. Este resultado eventualmente se asigna a la propiedad value que retorna yield. A Yield también podemos enviarle una función generadora y delegar la continuidad de la ejecución del código a otro generador. Esto se llama delegación de generadores porque un generador delega a otro generador la continuidad. Esto nos permite encadenar cuantos generadores queramos.

function* counter(){
  for(var i = 1; i <= 5; i++) {
    yield i;
  }
}

function* retornador() {
  yield* counter();
  console.log('regresé);
  yield 3;
}

let g = retornador();

console.log(g.next()); // { done: false, value: 1};
console.log(g.next()); // { done: false, value: 2};
console.log(g.next()); // { done: false, value: 3};
console.log(g.next()); // { done: false, value: 4};
console.log(g.next()); // { done: false, value: 5};
console.log(g.next()); // log 'Regresé' and { done: true, value: undefined }; // si no hay más yield
console.log(g.next()); // log 'Regresé' and { done: false, value: 3 }; // cuando hay otro yield

Iterables con iteradores

Los iterables (arreglos, cadenas, etc) nos permiten con el protocolo iterable definir el comportamiento de uno objetos cuando lo pasamos por un ciclo for of.

let counter(){
  for(var i = 1; i <= 5; i++){
    yield i;
  }
}

let numeros = [2, 5, 10];
for (numero of numeros) { console.log(numero); } // 2, 5, 10

let generator = counter();
for (numero of generator) { console.log(numero); } // 1, 2, 3, 4, 5

Para que un objeto sea iterable, necesita implementar un metodo llamado @@iterator, este metodo está identificado en los objetos por un simbolo, y no por una cadena. Lo encontramos en la constante Symbol.iterator del lenguage. Este es uno de los well-known symbols. Lo importante de este método, es que debe retornar un objeto iterator (que tenga un método next, y que retorne un objeto con las props value y done).

// iterator working
let contador = {
  [Symbol.iterator]() { // implements of @@iterator and return an iterator
    return {
      currentValue: 1,
      next() {
        let result = { value: null, done: false};
        if (this.currentValue > 0 && this.currentValue <= 5) {
          result = { value: this.currentValue, done: false };
          this.currentValue += 1;
        } else {
          result = { done: true };
        }
        return result;
      }
    }
  }
}

for (numero of contador) { console.log(numero) }; // 1, 2, 3, 4, 5

Rangos

Esto nos habilita a crear esta funcionalidad de otros lenguages

// rango con iterador
let rango = {
  min: null, 
  max: null,
  currentValue: null,
  next() {
    if (this.currentValue == null) tihs.currentValue = this.min;
    let result = {};
    if (this.currentValue >= this.min && this.currentValue <= this.max) {
      result = { value: this.currentValue, done: false };
      this.currentValue += 1;
    } else {
      result = { done: true };
    }
    return result;
  },
  [Symbol.iterator]() {
    return this;
  }
}

rango.min = 0;
rango.max = 10;
for (n of rango) { console.log(n) }
// rango con generador

let rango = {
  min: null,
  max: null,
  [Symbol.iterator]() {
    return this.generator();
  }
  generator: function*() {
    for (var i = this.min; i <= this.max; i++) {
      yield i;
    }
  }
}
rango.min = 0;
rango.max = 10;
for (n of rango) { console.log(n) }

La sintaxis de los generadores es considerablemente más simple y clara, no tienes que estar controlando el estado, ya que el generador lo hace por si mismo. No tenemos que definir explicitamente la estructura de value y done.