Normal Functions vs Generators Generators 部分で Finish 直前の yield-resume-yield ループを閉じる点線矢印に注目してください。 ジェネレーターは決して終了しない可能性があります。
ジェネレータの作成
JavaScript でジェネレータを作成する方法を見てみましょう –
function * generatorFunction() { // Line 1
console.log('This will be executed first.');
yield 'Hello, '; // Line 2 console.log('I will be printed after the pause');
yield 'World!';
}const generatorObject = generatorFunction(); // Line 3console.log(generatorObject.next().value); // Line 4
console.log(generatorObject.next().value); // Line 5
console.log(generatorObject.next().value); // Line 6// This will be executed first.
// Hello,
// I will be printed after the pause
// World!
// undefined
太字部分に注目してください。 ジェネレーター関数を作成するために、単なる function
ではなく function *
構文を使用します。 function
キーワード、*
、関数名の間には、いくつでも空白を入れることができます。
関数本体の内部には return
はありません。 その代わりに、別のキーワードyield
があります(2行目)。 これはジェネレータが自分自身を一時停止させることができる演算子です。 ジェネレータがyield
に遭遇するたびに、その後に指定された値を「返す」のです。 この場合、Hello,
が返されます。 しかし、ジェネレータの文脈では「返される」とは言いません。 2899>
ジェネレータから戻ることもできる。 しかし、return
はdone
プロパティをtrue
に設定し、その後ジェネレータはそれ以上値を生成することができなくなります。 関数 generatorFunction
を呼び出しているように見えます。 確かにそうです。 違いは、ジェネレータ関数は値を返す代わりに、常にジェネレータオブジェクトを返すことです。 ジェネレータ・オブジェクトはイテレータです。
4 行目では、generatorObject
の next()
メソッドを呼び出しています。 この呼び出しで、ジェネレータは実行を開始します。 まず、This will be executed first.
をconsole.log
し、yield 'Hello, '
に遭遇する。 ジェネレータはその値をオブジェクト{ value: 'Hello, ', done: false }
として生成し、一時停止する。 2899>
5行目で再びnext()
を呼び出している。 今度はジェネレータが目を覚まし、出発したところから実行を始める。 次に見つかった行はconsole.log
である。 文字列 I will be printed after the pause
をログに記録しています。 さらにyield
が見つかりました。 値はオブジェクト { value: 'World!', done: false }
として出力されます。 value
プロパティを抽出し、それをログに記録します。 2899>
6行目で再びnext()
を呼び出しています。 今回はもう実行する行はない。 どの関数もreturn文が提供されないと暗黙のうちにundefined
を返すことを思い出してください。 したがって、ジェネレータはオブジェクト{ value: undefined, done: true}
を(yieldの代わりに)返している。 done
には true
がセットされる。 これはこのジェネレータの終わりを告げるものである。
ジェネレーターを再び実行するには、新しい別のジェネレーター オブジェクトを作成する必要があります。
イテレータブルの実装
イテレータを実装する場合、next()
メソッドを持つイテレータオブジェクトを手動で作成する必要があります。 また、手動で状態を保存する必要があります。 多くの場合、それを行うのは本当に難しくなります。 ジェネレータはイテレータブルでもあるので、余分な定型的なコードなしにイテレータブルを実装するのに使うことができます。
問題: This
、is
、iterable.
を返すカスタムの反復処理関数を作成したい。 以下はイテレータを使った実装です –
const iterableObj = {
() {
let step = 0;
return {
next() {
step++;
if (step === 1) {
return { value: 'This', done: false};
} else if (step === 2) {
return { value: 'is', done: false};
} else if (step === 3) {
return { value: 'iterable.', done: false};
}
return { value: '', done: true };
}
}
},
}for (const val of iterableObj) {
console.log(val);
}// This
// is
// iterable.
ジェネレータを使った同じものです –
function * iterableObj() {
yield 'This';
yield 'is';
yield 'iterable.'
}for (const val of iterableObj()) {
console.log(val);
}// This
// is
// iterable.
両方のバージョンを比較してみてください。 確かに、これはある種の作為的な例です。
-
Symbol.iterator
-
next()
.
を実装する必要はなく、手動で next()
の戻りオブジェクト、つまり { value: 'This', done: false }
.
- を作る必要はなく、状態を保存しなくて良い。 イテレータの例では、状態は変数
step
に保存されていた。 その値がイテレータブルから出力されるものを定義していた。
Better Async functionality
function fetchJson(url) {
return fetch(url)
.then(request => request.text())
.then(text => {
return JSON.parse(text);
})
.catch(error => {
console.log(`ERROR: ${error.stack}`);
});
}
のような約束とコールバックを使用するコードは、(co.js などのライブラリの助けを借りて)
const fetchJson = co.wrap(function * (url) {
try {
let request = yield fetch(url);
let text = yield request.text();
return JSON.parse(text);
}
catch (error) {
console.log(`ERROR: ${error.stack}`);
}
});
と書くことができ、async/await
を使うことと類似していると気付いた読者がいるかもしれない。 これは偶然の一致ではありません。 async/await
は同様の戦略をとることができ、約束が関係する場合には yield を await
に置き換えることができる。 ジェネレータに基づくことができる。 2899>
Infinite Data Streams
決して終わらないジェネレータを作成することが可能です。
function * naturalNumbers() {
let num = 1;
while (true) {
yield num;
num = num + 1
}
}const numbers = naturalNumbers();console.log(numbers.next().value)
console.log(numbers.next().value)// 1
// 2
ジェネレータ naturalNumbers
を作成します。 この関数の内部で、while
無限ループが発生します。 そのループの中で、num
をyield
します。 ジェネレータが降伏すると、中断される。 再びnext()
を呼び出すと、ジェネレータは目を覚まし、中断していた場所(この場合はyield num
)から継続し、別のyield
に遭遇するかジェネレータが終了するまで実行される。 次の文はnum = num + 1
であるので、num
を更新する。 そして、whileループの先頭に行く。 条件はまだ真である。 次の行 yield num
に進む。 更新されたnum
を出力し、中断する。
オブザーバとしてのジェネレータ
ジェネレータもnext(val)
関数を使って値を受け取ることができる。 そして、ジェネレータは新しい値を受け取ったときに目を覚ますので、オブザーバと呼ばれる。 ある意味、それは値のために観察し続け、1つを得るとき作用する。 このパターンについて詳しくは、こちらを参照してください。
ジェネレーターの利点
Infinite Data Streams の例で見られるように、これは遅延評価によってのみ可能である。 遅延評価とは、値が必要になるまで式の評価を遅らせる評価モデルです。 つまり、値が必要でなければ存在しないことになります。 要求に応じて計算される。 例を見てみよう –
function * powerSeries(number, power) {
let base = number;
while(true) {
yield Math.pow(base, power);
base++;
}
}
powerSeries
は、ある数を累乗にした級数を与える。 例えば、3の2乗の級数は、9(3²) 16(4²) 25(5²) 36(6²) 49(7²)となります。 const powersOf2 = powerSeries(3, 2);
を実行すると、ジェネレータオブジェクトを作成するだけです。 どの値も計算されていません。
Memory Efficient
Lazy Evaluation の直接的な結果は、ジェネレータがメモリ効率的であることです。 私たちは必要な値のみを生成します。 通常の関数では、すべての値を事前に生成し、後で使用する場合に備えてそれらを保持しておく必要がありました。 しかし、ジェネレーターを使用すると、必要なときまで計算を延期できます。
ジェネレーターで動作するコンビネーター関数を作成することができます。 コンビネータとは、既存の反復記号を組み合わせて新しい反復記号を作成する関数である。 これは反復記号の最初のn
要素を取る。
function * take(n, iter) {
let index = 0;
for (const val of iter) {
if (index >= n) {
return;
}
index = index + 1;
yield val;
}
}
take
の興味深い使用例をいくつか紹介します。
take(3, )// a b ctake(7, naturalNumbers());// 1 2 3 4 5 6 7take(5, powerSeries(3, 2));// 9 16 25 36 49
cycled library の実装(反転機能なし)です。 一度すべての値を使い切ると、再び反復することはできません。 値を再び生成するには、新しいジェネレータオブジェクトを作る必要があります。
const numbers = naturalNumbers();console.log(...take(10, numbers)) // 1 2 3 4 5 6 7 8 9 10
console.log(...take(10, numbers)) // This will not give any data
- ジェネレータオブジェクトは、配列で可能なランダムアクセスを許しません。 値は1つずつ生成されるので、ランダムな値にアクセスすると、その要素までの値を計算することになります。
結論
ジェネレーターでは、まだ多くのことがカバーされていません。 yield *
, return()
, throw()
のようなものです。 ジェネレータはまた、コルーチンを可能にします。
Python の itertools ページに移動して、イテレータとジェネレータで作業できるユーティリティのいくつかを見ることができます。 練習として、あなた自身でユーティリティを実装することができます。