効果的なプログラミング。パート1:イテレーターとジェネレーター

Javascriptは、多くのサイト(Githubなど)のバージョンによると、現在最も人気のあるプログラミング言語です。同時に、それは最も進んだ言語ですか、それとも好きな言語ですか?それは他の言語にとって不可欠な部分である構造を欠いています:広範な標準ライブラリ、不変性、マクロ。しかし、私の意見では、十分な注意が払われていない詳細が1つあります。それは、ジェネレーターです。



さらに、読者には、肯定的な反応の場合、サイクルに発展する可能性のある記事が提供されます。私がこのサイクルを首尾よく書き、リーダーがそれを首尾よく習得した場合、それが何をするかだけでなく、それが内部でどのように機能するかについても次のコードについて明らかになるでしょう:



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


これは最初のパイロット部分です:イテレーターとジェネレーター。



イテレーター



したがって、イテレータは、データへの順次アクセスを提供するインターフェイスです。



ご覧のとおり、この定義はデータやメモリ構造については何も述べていません。実際、未定義のシーケンスは、メモリスペースを占有せずにイテレータとして表すことができます。



私は読者に質問に答えることを提案します:配列はイテレーターですか?



回答
. shift pop .



では、言語の基本構造の1つである配列を使用して、データを順番に、または任意の順序で操作できるのに、なぜ反復子が必要なのでしょうか。



一連の自然数を実装するイテレーターが必要だと想像してみましょう。またはFibonacci番号。または他の無限のシーケンス。無限のシーケンスを配列に配置することは困難です。配列にデータを徐々に埋めるメカニズムと、プロセスメモリ全体を埋めないように古いデータを削除するメカニズムが必要です。これは不必要な複雑さであり、配列のないソリューションは複数の行に収まる可能性があるにもかかわらず、実装とサポートがさらに複雑になります。



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


また、イテレータは、Webソケットなどの外部チャネルからのデータの受信を表すことができます。



javascriptでは、イテレーターは、フィールド値(イテレーターの現在の値とdone)を持つ構造を返すnext()メソッドを持つオブジェクトです(この規則はECMAScript言語標準で説明されています)。このようなオブジェクトは、Iteratorインターフェイスを実装します。前の例を次の形式で書き直してみましょう。



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


JavascriptにはIterableインターフェイスもあります。これは、イテレーターを返す@@イテレーターメソッド(この定数はSymbol.iteratorとして使用可能)を持つオブジェクトですこのようなインターフェイスを実装するオブジェクトの場合、オペレータートラバーサルを使用できますfor..ofもう一度例を書き直してみましょう。今回は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


ご覧のとおり、ある時点で完了フラグを正にする必要がありました。そうしないと、ループが無限になります。



ジェネレーター



ジェネレーターは、イテレーターの進化における次の段階になりました。それらは、関数値のような反復子値を返すための構文上の砂糖を提供します。ジェネレーターは、イテレーターを返す関数(アスタリスク:function *で宣言)です。この場合、イテレーターは明示的に返されません。関数は、yieldステートメントを使用してイテレーターの値のみを返します関数が実行を終了すると、イテレーターは完了したと見なされます(次のメソッドへの後続の呼び出しの結果には、doneフラグがtrueに等しくなります)



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

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


すでにこの単純な例では、ジェネレーターの主なニュアンスが目に見えています。ジェネレーター関数内のコードは同期的に実行されません対応するイテレーターでnext()を呼び出した結果、ジェネレーターコードが段階的に実行されます。前の例でジェネレータコードがどのように実行されるかを見てみましょう。ジェネレータが停止した場所をマークするために、特別なカーソルを使用します。



naturalRowGeneratorが呼び出されると、イテレーターが作成されます。



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


さらに、最初の3回次のメソッドを呼び出すか、この場合はループを繰り返すと、カーソルはyieldステートメントの後に配置されます。



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


そして、nextへの後続のすべての呼び出しについて、およびループを終了した後、ジェネレーターは実行を終了し、nextを呼び出した結果は次のようになります。 { value: undefined, done: true }



イテレーターへのパラメーターの受け渡し



現在のカウンターをリセットし、最初からカウントを開始する機能を自然数のイテレーターに追加する必要があると想像してみましょう。



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


自作のイテレーターでそのようなパラメーターを処理する方法は明らかですが、ジェネレーターはどうですか?

ジェネレーターはパラメーターの受け渡しをサポートしていることがわかりました。



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


渡されたパラメーターは、yieldステートメントの結果として使用可能になります。カーソルアプローチで明快さを加えてみましょう。イテレーターが作成されたとき、何も変更されていません。これに続いて、next()メソッドへの最初の呼び出しが行われます。



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


カーソルは、yieldステートメントから戻った瞬間にフリーズしました。nextへの次の呼び出しで、関数に渡された値がリセット変数の値を設定します。まだyieldの呼び出しがないので、次の最初の呼び出しで渡された値はどこに行き着くのでしょうか。どこにも!広大なガベージコレクターに溶け込みます。ジェネレーターに初期値を渡す必要がある場合は、ジェネレーター自体の引数を使用してこれを行うことができます。例:



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


結論



イテレーターの概念とjavascript言語での実装について説明しました。また、ジェネレーター(イテレーターを便利に実装するための構文構造)についても学習しました。



この記事では番号シーケンスの例を示しましたが、javascriptイテレーターはさらに多くのことを実行できます。それらは、データの任意のシーケンス、さらには多くの有限状態マシンを表すことができます。次の記事では、ジェネレーターを使用して非同期プロセス(coroutines、goroutines、cspなど)を構築する方法について説明します。



All Articles