Effektive Programmierung. Teil 1: Iteratoren und Generatoren

Javascript ist derzeit die beliebteste Programmiersprache nach Versionen vieler Websites (z. B. Github). Ist es gleichzeitig die fortschrittlichste oder beliebteste Sprache? Es fehlen Konstrukte, die integraler Bestandteil anderer Sprachen sind: eine umfangreiche Standardbibliothek, Unveränderlichkeit, Makros. Aber es gibt ein Detail, das meiner Meinung nach nicht genügend Beachtung findet - die Generatoren.



Ferner wird dem Leser ein Artikel angeboten, der sich im Falle einer positiven Antwort zu einem Zyklus entwickeln kann. Wenn ich diesen Zyklus erfolgreich schreibe und der Reader ihn erfolgreich gemeistert hat, wird nicht nur klar, was er tut, sondern auch, wie er unter der Haube funktioniert:



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


Dies ist der erste Pilotteil: Iteratoren und Generatoren.



Iteratoren



Ein Iterator ist also eine Schnittstelle , die sequentiellen Zugriff auf Daten bietet.



Wie Sie sehen können, sagt die Definition nichts über Daten oder Speicherstrukturen aus. In der Tat kann eine Folge von undefinierten s als Iterator dargestellt werden, ohne Speicherplatz zu beanspruchen.



Ich schlage dem Leser vor, die Frage zu beantworten: Ist ein Array ein Iterator?



Antworten
. shift pop .



Warum werden dann Iteratoren benötigt, wenn ein Array, eine der Grundstrukturen der Sprache, es Ihnen ermöglicht, sowohl sequentiell als auch in beliebiger Reihenfolge mit Daten zu arbeiten?



Stellen wir uns vor, wir brauchen einen Iterator, der eine Folge natürlicher Zahlen implementiert. Oder Fibonacci-Zahlen. Oder irgendeine andere endlose Sequenz. Es ist schwierig, eine endlose Sequenz in einem Array zu platzieren. Sie benötigen einen Mechanismus, um das Array schrittweise mit Daten zu füllen und alte Daten zu entfernen, um nicht den gesamten Prozessspeicher zu füllen. Dies ist eine unnötige Komplikation, die eine zusätzliche Komplexität bei Implementierung und Support mit sich bringt, obwohl eine Lösung ohne Array in mehrere Zeilen passen kann:



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


Ein Iterator kann auch das Empfangen von Daten von einem externen Kanal wie einem Websocket darstellen.



In Javascript ist ein Iterator ein Objekt mit einer next () -Methode, die eine Struktur mit dem Feldwert - dem aktuellen Wert des Iterators und done - zurückgibt - einem Flag, das das Ende der Sequenz angibt (diese Konvention ist im ECMAScript- Sprachstandard beschrieben ). Ein solches Objekt implementiert die Iterator-Schnittstelle. Schreiben wir das vorherige Beispiel in diesem Format neu:



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


Javascript verfügt auch über eine Iterable-Schnittstelle. Hierbei handelt es sich um ein Objekt mit einer @@ Iterator- Methode (diese Konstante ist als Symbol.iterator verfügbar), die einen Iterator zurückgibt. Für Objekte, die eine solche Schnittstelle implementieren, steht die Operator-Durchquerung zur Verfügung for..of. Lassen Sie uns unser Beispiel noch einmal umschreiben, nur diesmal als iterierbare Implementierung:



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


Wie Sie sehen können, mussten wir das Fertig-Flag irgendwann positiv machen, sonst wäre die Schleife unendlich.



Generatoren



Generatoren wurden die nächste Stufe in der Entwicklung von Iteratoren. Sie stellen syntaktischen Zucker bereit, um Iteratorwerte wie einen Funktionswert zurückzugeben. Ein Generator ist eine Funktion (mit einem Sternchen gekennzeichnet: Funktion * ), die einen Iterator zurückgibt. In diesem Fall wird der Iterator nicht explizit zurückgegeben, die Funktionen geben nur die Werte des Iterators mithilfe der Yield-Anweisung zurück . Wenn die Ausführung der Funktion abgeschlossen ist, wird der Iterator als abgeschlossen betrachtet (bei den Ergebnissen nachfolgender Aufrufe der nächsten Methode ist das Flag done gleich true).



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

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


Bereits in diesem einfachen Beispiel ist die Hauptnuance von Generatoren mit bloßem Auge sichtbar: Der Code in der Generatorfunktion wird nicht synchron ausgeführt . Der Generatorcode wird schrittweise ausgeführt, wenn next () auf dem entsprechenden Iterator aufgerufen wird. Mal sehen, wie der Generatorcode im vorherigen Beispiel ausgeführt wird. Wir werden einen speziellen Cursor verwenden, um zu markieren, wo der Generator gestoppt hat.



Wenn naturalRowGenerator aufgerufen wird, wird ein Iterator erstellt.



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


Wenn wir die nächste Methode die ersten drei Male aufrufen oder in unserem Fall die Schleife durchlaufen, wird der Cursor nach der Yield-Anweisung positioniert.



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


Und für alle nachfolgenden Aufrufe von next und nach dem Verlassen der Schleife schließt der Generator seine Ausführung ab und die Ergebnisse des nächsten Aufrufs sind { value: undefined, done: true }



Übergabe von Parametern an einen Iterator



Stellen wir uns vor, wir müssen die Möglichkeit hinzufügen, den aktuellen Zähler zurückzusetzen und von Anfang an mit dem Zählen zu unserem Iterator natürlicher Zahlen zu beginnen.



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


Es ist klar, wie ein solcher Parameter in einem selbstgeschriebenen Iterator behandelt wird, aber was ist mit Generatoren?

Es stellt sich heraus, dass Generatoren die Parameterübergabe unterstützen!



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


Der übergebene Parameter wird als Ergebnis der Yield-Anweisung zur Verfügung gestellt. Versuchen wir, mit einem Cursor-Ansatz Klarheit zu schaffen. Als der Iterator erstellt wurde, hat sich nichts geändert. Darauf folgt der erste Aufruf der next () -Methode:



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


Der Cursor fror im Moment der Rückkehr von der Ertragsangabe ein. Beim nächsten Aufruf zum nächsten setzt der an die Funktion übergebene Wert den Wert der Rücksetzvariablen. Wo endet der Wert, der an den allerersten Aufruf bis zum nächsten übergeben wird, da noch kein Aufruf zum Nachgeben erfolgt ist? Nirgends! Es wird sich in der Weite des Müllsammlers auflösen. Wenn Sie einen Anfangswert an den Generator übergeben müssen, können Sie dies mit den Argumenten des Generators selbst tun. Beispiel:



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


Fazit



Wir haben das Konzept der Iteratoren und seine Implementierung in der Javascript-Sprache diskutiert. Wir haben auch Generatoren untersucht - ein syntaktisches Konstrukt zur bequemen Implementierung von Iteratoren.



Obwohl ich in diesem Artikel Beispiele mit Zahlenfolgen angegeben habe, können Javascript-Iteratoren viel mehr. Sie können eine beliebige Datenfolge und sogar viele endliche Zustandsmaschinen darstellen. Im nächsten Artikel möchte ich darüber sprechen, wie Sie mit Generatoren asynchrone Prozesse erstellen können (Coroutinen, Goroutinen, csp usw.).



All Articles