Programación eficaz. Parte 1: iteradores y generadores

Javascript es actualmente el lenguaje de programación más popular según las versiones de muchos sitios (por ejemplo, Github). Al mismo tiempo, ¿es el idioma más avanzado o favorito? Carece de construcciones que sean partes integrales de otros lenguajes: una biblioteca estándar extensa, inmutabilidad, macros. Pero hay un detalle en él que, en mi opinión, no recibe suficiente atención: los generadores.



Además, se ofrece al lector un artículo que, en el caso de una respuesta positiva, puede convertirse en un ciclo. Si escribo con éxito este ciclo y el lector lo ha dominado con éxito, quedará claro sobre el siguiente código no solo lo que hace, sino también cómo funciona bajo el capó:



while (true) {
    const data = yield getNextChunk(); //   
    const processed = processData(data);
    try {
        yield sendProcessedData(processed);
        showOkResult();
    } catch (err) {
        showError();
    }
}


Esta es la primera parte piloto: iteradores y generadores.



Iteradores



Entonces, un iterador es una interfaz que proporciona acceso secuencial a los datos.



Como puede ver, la definición no dice nada sobre datos o estructuras de memoria. De hecho, una secuencia de s indefinidos se puede representar como un iterador sin ocupar espacio en la memoria.



Sugiero al lector que responda la pregunta: ¿una matriz es un iterador?



Responder
. shift pop .



Entonces, ¿por qué se necesitan iteradores si una matriz, una de las estructuras básicas del lenguaje, le permite trabajar con datos tanto secuencialmente como en orden arbitrario?



Imaginemos que necesitamos un iterador que implemente una secuencia de números naturales. O números de Fibonacci. O cualquier otra secuencia sin fin . Es difícil colocar una secuencia interminable en una matriz; necesita un mecanismo para llenar gradualmente la matriz con datos, así como para eliminar los datos antiguos para no llenar toda la memoria del proceso. Esta es una complicación innecesaria, que conlleva una complejidad adicional de implementación y soporte, a pesar de que una solución sin un arreglo puede caber en varias líneas:



const getNaturalRow = () => {
    let current = 0;
    return () => ++current;
};


Además, un iterador puede representar la recepción de datos de un canal externo, como un websocket.



En javascript, un iterador es cualquier objeto que tiene un método next () que devuelve una estructura con el valor de los campos, el valor actual del iterador y listo, una bandera que indica el final de la secuencia (esta convención se describe en el estándar de lenguaje ECMAScript ). Dicho objeto implementa la interfaz Iterator. Reescribamos el ejemplo anterior en este formato:



const getNaturalRow = () => ({
    _current: 0,
    next() { return {
        value: ++this._current,
        done: false,
    }},
});


Javascript también tiene una interfaz Iterable, que es un objeto que tiene un método @@ iterator (esta constante está disponible como Symbol.iterator) que devuelve un iterador. Para los objetos que implementan una interfaz de este tipo, el operador transversal está disponible for..of. Reescribamos nuestro ejemplo una vez más, solo que esta vez como una implementación iterable:



const naturalRowIterator = {
    [Symbol.iterator]: () => ({
        _current: 0,
        next() { return {
            value: ++this._current,
            done: this._current > 3,
       }},
   }),
}

for (num of naturalRowIterator) {
    console.log(num);
}
// : 1, 2, 3


Como puede ver, tuvimos que hacer que la bandera de listo en algún momento se volviera positiva, de lo contrario el ciclo sería infinito.



Generadores



Los generadores se convirtieron en la siguiente etapa en la evolución de los iteradores. Proporcionan azúcar sintáctica para devolver valores de iterador como un valor de función. Un generador es una función (declarada con un asterisco: función * ) que devuelve un iterador. En este caso, el iterador no se devuelve explícitamente, las funciones solo devuelven los valores del iterador utilizando la declaración de rendimiento . Cuando la función finaliza su ejecución, el iterador se considera completo (los resultados de las llamadas posteriores al siguiente método tendrán el indicador de hecho igual a verdadero)



function* naturalRowGenerator() {
    let current = 1;
    while (current <= 3) {
        yield current;
        current++;
    }
}

for (num of naturalRowGenerator()) {
    console.log(num);
}
// : 1, 2, 3


Ya en este sencillo ejemplo, el principal matiz de los generadores es visible a simple vista: el código dentro de la función del generador no se ejecuta sincrónicamente . El código del generador se ejecuta en etapas, como resultado de las llamadas a next () en el iterador correspondiente. Veamos cómo se ejecuta el código del generador en el ejemplo anterior. Usaremos un cursor especial para marcar dónde se detuvo el generador.



Cuando se llama a naturalRowGenerator, se crea un iterador.



function* naturalRowGenerator() {let current = 1;
    while (current <= 3) {
        yield current;
        current++;
    }
}


Además, cuando llamamos al método siguiente las tres primeras veces o, en nuestro caso, iteramos a través del ciclo, el cursor se coloca después de la declaración de rendimiento.



function* naturalRowGenerator() {
    let current = 1;
    while (current <= 3) {
        yield current; ▷
        current++;
    }
}


Y para todas las llamadas posteriores a next y después de salir del bucle, el generador completa su ejecución y los resultados de la llamada next serán { value: undefined, done: true }



Pasando parámetros a un iterador



Imaginemos que necesitamos agregar la capacidad de restablecer el contador actual y comenzar a contar desde el principio hasta nuestro iterador de números naturales.



naturalRowIterator.next() // 1
naturalRowIterator.next() // 2
naturalRowIterator.next(true) // 1
naturalRowIterator.next() // 2


Está claro cómo manejar ese parámetro en un iterador autoescrito, pero ¿qué pasa con los generadores?

¡Resulta que los generadores admiten el paso de parámetros!



function* naturalRowGenerator() {
    let current = 1;
    while (true) {
        const reset = yield current;
        if (reset) {
          current = 1;
        } else {
          current++;
        }
    }
}


El parámetro pasado está disponible como resultado de la declaración de rendimiento. Intentemos agregar claridad con un enfoque de cursor. Cuando se creó el iterador, nada cambió. A esto le sigue la primera llamada al método next ():



function* naturalRowGenerator() {
    let current = 1;
    while (true) {
        const reset = ▷yield current;
        if (reset) {
          current = 1;
        } else {
          current++;
        }
    }
}


El cursor se congeló en el momento de regresar de la declaración de rendimiento. En la siguiente llamada a next, el valor pasado a la función establecerá el valor de la variable de reinicio. ¿Dónde termina el valor pasado en la primera llamada a la siguiente, ya que aún no ha habido una llamada a ceder? ¡En ninguna parte! Se disolverá en la inmensidad del recolector de basura. Si necesita pasar algún valor inicial al generador, entonces puede hacerlo usando los argumentos del propio generador. Ejemplo:



function* naturalRowGenerator(start = 1) {
    let current = start;
    while (true) {
        const reset = yield current;
        if (reset) {
          current = start;
        } else {
          current++;
        }
    }
}

const iterator = naturalRowGenerator(10);
iterator.next() // 10
iterator.next() // 11
iterator.next(true) // 10


Conclusión



Hemos discutido el concepto de iteradores y su implementación en el lenguaje javascript. También estudiamos generadores, una construcción sintáctica para implementar iteradores convenientemente.



Aunque he proporcionado ejemplos con secuencias numéricas en este artículo, los iteradores de JavaScript pueden hacer mucho más. Pueden representar cualquier secuencia de datos e incluso muchas máquinas de estados finitos. En el próximo artículo, me gustaría hablar sobre cómo se pueden usar generadores para construir procesos asincrónicos (corrutinas, gorutinas, csp, etc.).



All Articles