Un repaso a los iteradores y generadores de ES6

En un post anterior le dimos un repaso a las promesas de ES6 y como ya comenté ahora le toca el turno a los iteradores y generadores.

La generación de datos de manera secuencial es algo muy común a la hora de escribir un programa y hasta ahora este tipo de operaciones solo se podían conseguir mediante el uso de ciclos de repetición como los bucles for o while pero con la versión 6 de ECMAScript entran en juego los generadores con sus inseparables amigos, los iteradores.

 

un-repaso-a-los-generadores-en-es6

 

Iterables

Un objeto iterable es aquel que implementa la interface Iterable, y que por tanto expone un método predeterminado para iterar sobre el además de especificar su comportamiento de iteración. JavaScript cuenta con varios objetos iterables como por ejemplo String, Array, Map y Set.

Como todos sabemos podemos iterar sobre los items de un Array por medio de un bucle for…of de la siguiente manera.

let arr = [1, 2, 3];
for (let item of arr) {
    console.log(item)  // 1, 2, 3
}

Sin saberlo estamos haciendo un uso implícito del método [Symbol.iterator] heredado del prototipo del objeto Array, pero ni todos los objetos son iterables ni todos los objetos iterables pueden ser iterados de la misma manera. Si intentásemos recorrer un Object con un bucle for…of el interprete seguramente se reirá de nosotros y nos mostrará el siguiente error:

Uncaught TypeError: iterable[Symbol.iterator] is not a function(…)

Esto es así porque Object no puede ser iterado por medio de este tipo de bucle, pero gracias a que la interfaz Iterable permite definir y personalizar el orden de iteración podemos tomar prestado el comportamiento de iteración de Array para implementarlo en Object.

const iterableObject = {
  0: 'value 0',
  1: 'value 1',
  2: 'value 2',
  3: 'value 3',
  length: 4,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
}
for (let item of iterableObject) {
    console.log(item)  // value 0, value 1, value 2, value 3,
}
console.log(iterableObject.length)  // 4

De igual manera podríamos convertir un Array en un objeto iterable a través de dicha interfaz para poder recorrer todos sus items con el método next().

const arr = ['1', '2', '3']
const iter = arr[Symbol.iterator]()
console.log(iter.next())  // Object {value: "1", done: false}

Iteradores

Los iteradores implementan la interfaz Iterator y vienen a ser un tipo de objeto que sabe como acceder a los items de una colección o secuencia uno a uno y a su vez mantiene la referencia a su posición actual en dicha secuencia.

Podemos crear nuestros propios iteradores implementando un método next() que nos devuelva un objeto con dos propiedades, value y done.

const iterator = arr => {
  let index = 0
  return {
    next () {
      return index < arr.length ?
        {value: arr[index++], done: false} :
        {done: true}
     }
   }
 }

let i = iterator([1, 2, 3, 4])
console.log(i.next())  // Object {value: 1, done: false}

Como se puede ver en el ejemplo, hemos creado nuestro objeto iterator que recoge un Array como argumento y lo convierte en un objeto sobre el que podemos iterar a través del método next() de la interfaz Iterable. Si retomamos el ejemplo de más arriba en el que convertimos un Object en un objeto iterable del mismo modo que un Array, podríamos extenderlo un poco más para poder recorrer sus items con next().

let iterableObject = {
  0: 'value 0',
  1: 'value 1',
  2: 'value 2',
  3: 'value 3',
  length: 4,
  [Symbol.iterator]() {
    let index = 0
    return {
      next: () => {
        let value = this[index]
        let done = index >= this. length
        index ++
        return { value, done }
      }
    }
  }
}

for (let item of iterableObject) {
  console.log(item)
}

let iterator = iterableObject[Symbol.iterator]()
console.log(iterator.next())

Como puedes ver nuestro objeto contiene el método [Symbol.iterator]() que es el que define que se trata de un objeto iterable, de este modo podemos iterar a través de el tanto con un bucle for…of como por medio del método next().

 

Generadores

Los generadores son un tipo función ‘especial’ con la peculiaridad de que pueden ser pausadas sin bloquear la ejecución del programa y ser reanudadas en otro momento y además siempre guardan una referencia a su contexto.

Esto quizás suene un poco ‘marciano’ pero veamos un poco de código y nos daremos cuenta de que no tiene tanto misterio.

La sintaxis es bastante simple, se declara como function* y debe contener al menos una sentencia yield.

function* generator () {
  let index = 0
  while (index < 3)
    yield index++
}

let g = generator()

console.log(g.next())  // Object {value: 0, done: false}
console.log(g.next())  // Object {value: 1, done: false}
console.log(g.next())  // Object {value: 2, done: false}
console.log(g.next())  // Object {value: undefined, done: true}

Paremonos a analizar como se ejecuta este código. Como podéis ver el cuerpo de la función generadora no se ejecuta completamente si no que devuelve un objeto iterador. Cuando llamamos al método next() de dicho objeto, el cuerpo de la función se ejecuta hasta llegar al yield, el cual especifica el valor del iterador y nos lo devuelve como si de un return se tratase.

Un dato importante que ya he comentado antes pero que me gustaría destacar, es que la función generadora guarda una referencia a su contexto, es por esto que cuando volvemos a llamar al método next() el valor devuelto es 1 y no otra vez 0.

Veamos un ejemplo un poco más ilustrativo. Vamos a crear un objeto que pueda iterar sobre los caracteres de una cadena con una función generadora.

function* stringIterable (string) {
    const str = string
    let id = 0
    while(id < str.length)
        yield str[id++]
}

const iterator = stringIterable('fanfuckingtastic')

for (item of iterator) {
  console.log(item)  // f, a, n, f, u, c, k, ...
}

console.log(iterator.next())  // {value: "f", done: false}
console.log(iterator.next())  // {value: "a", done: false}
console.log(iterator.next())  // {value: "n", done: false}

Por poner un ejemplo más, intentemos generar la secuencia Fibonacci por medio de generadores.

function* fib() {
  var current = a = b = 1
  yield 1

  while (true) {
    current = b
    yield current
    b = a + b
    a = current
  }
}

sequence = fib()

Como veis la función es bastante simple pero muy funcional ya que podemos iterar el objeto sequence de varias maneras.

for (item of sequence) { 
  console.log(item) // 1, 1, 2, 3, 5, 8, 13, ... 
}

console.log(sequence.next())  // Object {value: 1, done: false}¡

 

En JavaScript nos encantan las operaciones asíncronas, y es con este tipo de operaciones con las que los generadores se llevan bien. Al igual que ocurre con las promesas, los iteradoresgeneradores nos ayudan a escribir código asíncrono de manera síncrona, y eso es super guay.

En breve espero publicar algo sobre las funciones asíncronas con async y await de ES7 o es2016 y con esto dejar visto el tema de las operaciones asíncronas en JavaScript, al menos por el momento.

Espero que os haya resultado interesante. Hasta la próxima!