JavaScript/ジェネレータ
ジェネレータ(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
done
とvalue
編集
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
を活用することで、複雑な処理をシンプルに表現でき、非常に直感的に使うことができます。次の章では、ジェネレータをより高度に活用するためのテクニックについて解説します。