コンテンツにスキップ

JavaScript/ジェネレータ

出典: フリー教科書『ウィキブックス(Wikibooks)』

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