ジェネレータ(Generators)

編集

JavaScriptにおけるジェネレータは、関数を一時的に停止し、その状態を保存することができる特別な種類の関数です。これにより、関数が一度に全ての結果を返すのではなく、必要なときに逐次的に結果を返すことができます。この章では、ジェネレータの基本的な使い方、構文、そして実際の活用方法について学びます。

ジェネレータ関数の定義

編集

ジェネレータ関数は、function*という構文を使用して定義します。function*と宣言された関数は、呼び出すたびに「イテレータ」を返します。このイテレータは、yieldというキーワードを使って値を逐次的に返すことができます。

以下はジェネレータ関数の基本的な例です:

function* myGenerator() { yield 1; yield 2; yield 3; } 

上記の関数は、myGenerator()を呼び出すたびに、1、2、3の値を逐次的に返します。

ジェネレータ関数の実行

編集

ジェネレータ関数を呼び出すと、関数の実行は停止し、ジェネレータオブジェクトが返されます。このオブジェクトは、next()メソッドを持っており、呼び出すたびに次のyieldまで実行を進めます。

const gen = myGenerator(); console.log(gen.next().value); // 1 console.log(gen.next().value); // 2 console.log(gen.next().value); // 3 console.log(gen.next().value); // undefined 

next()は、valueプロパティとdoneプロパティを持つオブジェクトを返します。valueには返された値が入り、doneはジェネレータが終了したかどうかを示します。

const result = gen.next(); console.log(result.value); // 1 console.log(result.done); // false 

donevalue

編集
  • value: ジェネレータがyieldで返した値です。
  • done: ジェネレータが終了した場合にtrue、それ以外はfalseです。

ジェネレータ関数は、returnステートメントが呼ばれると終了し、doneプロパティがtrueになります。

function* myGenerator() { yield 1; return 2; } const gen = myGenerator(); console.log(gen.next()); // { value: 1, done: false } console.log(gen.next()); // { value: 2, done: true } console.log(gen.next()); // { value: undefined, done: true } 

yield キーワード

編集

yieldは、ジェネレータ関数の実行を一時停止し、関数外部に値を返します。yieldはその後、呼び出されたときに再開されます。再開された時、yieldは次の値を返すか、関数の実行を終了させます。

function* numbers() { yield 1; yield 2; yield 3; } const numGen = numbers(); console.log(numGen.next().value); // 1 console.log(numGen.next().value); // 2 console.log(numGen.next().value); // 3 

ジェネレータの活用例

編集

ジェネレータは、データの逐次的な生成や、非同期処理の扱いに非常に便利です。以下にいくつかの使用例を紹介します。

自然数のシーケンス

編集

自然数を逐次的に生成するジェネレータを作成できます。

function* naturals() { let i = 1; while (true) { yield i++; } } const gen = naturals(); console.log(gen.next().value); // 1 console.log(gen.next().value); // 2 console.log(gen.next().value); // 3 

フィボナッチ数列

編集

ジェネレータを使用して、フィボナッチ数列を逐次的に生成する例です。

function* fibonacci() { let [prev, curr] = [0, 1]; while (true) { yield curr; [prev, curr] = [curr, prev + curr]; } } const fib = fibonacci(); console.log(fib.next().value); // 1 console.log(fib.next().value); // 1 console.log(fib.next().value); // 2 console.log(fib.next().value); // 3 console.log(fib.next().value); // 5 

ジェネレータと範囲For

編集

JavaScriptでは、ジェネレータをfor...ofループに渡して、生成された値を繰り返し処理することができます。for...ofは、イテレータを返すオブジェクト(ジェネレータオブジェクトを含む)を反復処理する構文です。この機能を活用することで、ジェネレータ関数から逐次的に値を取得し、簡潔にループ処理を行うことができます。

以下の例は、ジェネレータとfor...ofループを組み合わせて、ジェネレータから返される値を順番に処理する方法を示しています。

function* numbers() { yield 1; yield 2; yield 3; } for (const num of numbers()) { console.log(num); } 

このコードでは、numbers()ジェネレータ関数が生成する値をfor...ofループで受け取り、1、2、3を順にコンソールに出力します。for...ofは、ジェネレータのnext()メソッドを自動的に呼び出して、yieldされた値を処理します。

範囲Forと無限ジェネレータ

編集

ジェネレータは無限に値を生成することもできます。例えば、無限の自然数を生成するジェネレータを考えた場合、for...ofループは、終了条件を指定するまで無限に値を処理し続けます。

function* naturals() { for (let i = 1; ; yield i++) { } } let count = 0; for (const num of naturals()) { console.log(num); count++; if (count === 5) break; // 5回で停止 } 

この例では、naturals()ジェネレータが無限に数を生成しますが、for...ofループ内で5回目の反復でbreakを使ってループを終了させています。

ジェネレータと範囲Forの利点

編集
  • コードの簡潔さ: for...ofループを使用することで、next()メソッドを明示的に呼び出すことなく、ジェネレータからの値を簡単に取り出して処理できます。
  • 遅延評価: ジェネレータが遅延評価をサポートしているため、for...ofループを使うことで、必要なタイミングで値を生成し、メモリ効率を高めることができます。
  • 無限データの処理: 無限に続くシーケンスを扱う場合でも、for...ofループは適切に終了条件を指定して処理できます。

ジェネレータとfor...ofループを組み合わせることで、効率的で読みやすいコードを書くことができ、特に遅延処理や無限シーケンスを扱う際に非常に便利です。

ジェネレータと非同期処理

編集

ジェネレータは、非同期処理を扱うためにも使用できます。yieldを使って非同期関数の結果を待つことができます。これを実現するためには、Promiseとの組み合わせが必要です。

function* fetchData() { const data1 = yield fetch('https://api.example.com/data1'); const data2 = yield fetch('https://api.example.com/data2'); return [data1, data2]; } const gen = fetchData(); gen.next().value.then(response => response.json()) .then(data1 => { console.log(data1); gen.next(data1).value.then(response => response.json()) .then(data2 => { console.log(data2); gen.next(data2); }); }); 

このように、yieldを使って非同期操作の結果を逐次的に処理できます。

実践的な実装例

編集
class Lazy { constructor(generator) { this.generator = generator; } // コレクションからLazyに変換する静的メソッド static fromArray(arr) { return new Lazy(function* () { for (const item of arr) { yield item; } }); } static naturals() { return new Lazy(function* () { for (let i = 1; ; yield i++){ } }); } static primes() { return new Lazy(function* () { const primes = []; for (let i = 2; ; i++) { let isPrime = true; for (const prime of primes) { if (i % prime === 0) { isPrime = false; break; } } if (isPrime) { primes.push(i); yield i; } } }); } filter(predicate) { const generator = this.generator; return new Lazy(function* () { for (const v of generator()) { if (predicate(v)) { yield v; } } }); } map(transform) { const generator = this.generator; return new Lazy(function* () { for (const v of generator()) { yield transform(v); } }); } reduce(initialValue, accumulator) { let result = initialValue; for (const v of this.generator()) { result = accumulator(result, v); } return result; } take(n) { const generator = this.generator; return new Lazy(function* () { let count = 0; for (const v of generator()) { if (count < n) { yield v; count++; } else { return; } } }); } array() { return [...this.generator()]; } // Symbol.iteratorを実装 [Symbol.iterator]() { return this.generator(); } } // メイン関数 console.log("ArrayからLazyへの変換:\t", Lazy.fromArray([1, 2, 3, 4, 5]).filter(x => x % 2 === 0).array()); console.log("最初の10個の自然数:\t", Lazy.naturals().take(10).array()); console.log("最初の10個の偶数: \t", Lazy.naturals().filter(x => x % 2 === 0).take(10).array()); console.log("最初の10個の素数: \t", Lazy.primes().take(10).array()); console.log("最初の10個の自然数の2乗:", Lazy.naturals().take(10).map(x => x * x).array()); console.log("最初の10個の偶数の3倍:\t", Lazy.naturals().filter(x => x % 2 === 0).take(10).map(x => x * 3).array()); console.log("最初の10個の自然数の和:\t", Lazy.naturals().take(10).reduce(0, (a, b) => a + b)); console.log("最初の10個の自然数の積:\t", Lazy.naturals().take(10).reduce(1, (a, b) => a * b)); // for...ofでLazyを使う例 console.log("for...of で最初の10個の自然数:"); for (const num of Lazy.naturals().take(10)) { console.log(num); } // ジェネレータを直接 for でイテレーション for (const num of Lazy.naturals()) { if (num > 5) break; console.log(num); } 

結論

編集

ジェネレータは、遅延評価、非同期処理、シーケンシャルなデータ生成など、さまざまな場面で役立つ強力な機能です。yieldを活用することで、複雑な処理をシンプルに表現でき、非常に直感的に使うことができます。次の章では、ジェネレータをより高度に活用するためのテクニックについて解説します。