ジェネレーター Generator
《 初回公開:2023/10/19 , 最終更新:未 》
【 目次 】
ジェネレータを使うとイテレータやイテラブル(オブジェクト)を簡単に実装することができる。
ジェネレータについてChatGPTに説明してもらうと
ジェネレータは、関数を定義するための特別な構文であり、イテレータを簡単に作成することができます。ジェネレータ関数は、functionキーワードの後に「*」を付けて定義されます。
また、ジェネレータ関数内ではyieldキーワードを使用して、値を逐次的に返すことができます。
yieldキーワードを使用して値を返すと、関数の実行は中断され、値が反復処理によって利用可能になるまで停止します。その後、関数は中断した場所から再開されます。以下は、ジェネレータ関数の例です。
function* myGenerator() { yield 1; yield 2; yield 3; } const myIterable = myGenerator(); console.log(myIterable.next().value); // 1 console.log(myIterable.next().value); // 2 console.log(myIterable.next().value); // 3
この例では、myGeneratorというジェネレータ関数が定義されています。myGenerator関数は、yieldキーワードを使用して、1, 2, 3という値を順番に返します。
myGenerator関数を呼び出して、イテレータオブジェクトを作成します。
イテレータオブジェクトを作成するには、関数を呼び出すだけで良いのです。
これは、ジェネレータ関数がイテレータオブジェクトを返すように設計されているためです。その後、myIterableオブジェクトのnext()メソッドを呼び出して、値を逐次的に取得します。
next()メソッドは、オブジェクトを返します。
そのオブジェクトには、valueプロパティに現在の値が格納されており、doneプロパティには、ジェネレータ関数がすべての値を返し終わったかどうかが示されています。
このようにして、ジェネレータを使用して、簡単にイテレータを作成することができます。
yield
yieldの概念は理解しづらいがreturn文のようなもので、 MDNのドキュメントを引用すると。
yield はそれを含むジェネレーター関数の中で直接しか呼び出すことしかできません。
呼び出し先の関数やコールバックから呼び出すことはできません。yield キーワードはジェネレーターの next() メソッドを呼び出させ、 IteratorResult オブジェクトを返します。
これには value と done の 2 つのプロパティがあります。
value プロパティは yield 式の評価結果であり、 done は false、すなわちジェネレーター関数が完全には完了していないことを示します。yield 式によって実行が停止されると、ジェネレーターの next() メソッドが呼び出されるまで、ジェネレーターのコード実行は一時停止します。
ジェネレーターの next() メソッドが呼ばれるたびに、ジェネレーターの実行が再開され、次のうちのいずれかに達するまで実行されます。
- ジェネレーターを再び停止して、ジェネレーターの新しい値を返す yield。再度 next() が呼ばれると yield の直後から実行が再開されます。
- ジェネレーターから例外を発生させるために使用される throw。完全にジェネレーターの実行を停止し、 (通常の例外が発生した場合のように) 呼び出し元で実行が再開されます。
- ジェネレーター関数の末尾。この場合、ジェネレーターの実行は終了し、 IteratorResult オブジェクトの value に undefined が、 done に true が代入されて呼び出し元に返されます。
- return ステートメント。この場合はジェネレーターの実行は終了し、 IteratorResult オブジェクトの value に return ステートメントで指定した値が、 done に true が代入されて呼び出し元に返されます。
ジェネレータ関数の動作
ジェネレーター関数
ジェネレータ関数はイテレータでありイテラブルである
ジェネレータ関数が返すオブジェクトは、反復可能プロトコルと反復子プロトコルの両方が実装されている。
const myIterable = myGenerator();
for(let value of myIterable)
console.log(value);
// 1
// 2
// 3
通常の関数と同様、ジェネレータ関数に引数を渡す事もできる。
// 引数付きのジェネレータ関数
function* generator(a) {
yield a;
yield a * 2;
}
let it = generator(3);
for (let v of it) {
console.log(v); // 3 6
}
無名のジェネレータ関数にする事も。
// 無名のジェネレータ関数
let generator = function* (a) {
yield a;
yield a * 2;
};
let it = generator(3);
for (let v of it) {
console.log(v); // 3 6
}
ジェネレータ関数のイテレータ,イテラブルオブジェクトとしての利用
ジェネレータ関数はイテレータブル,イテラブルオブジェクトとしての利用可能であるが、
ジェネレータ関数単独では再利用可能ではない。
一度しか反復することができない反復可能オブジェクト (例えば、ジェネレーター) は、通常 @@iterator メソッドから this を返します。
何度も繰り返し可能なものは、@@iterator の各呼び出しで新しいイテレーターを返す必要があります。
これはどういう事かというと、想像するに、
ジェネレータ関数の@@iteratorメソッドは自分自身(this)を返す。
そして、このメソッドが返すオブジェクトは再利用可能ではない。
const generator = myGenerator();
// ジェネレータ関数の@@iteratorメソッドは自分自身を返す。
console.log(generator[Symbol.iterator]() === generator); // true
// 一度しか反復できない
for (let value of generator)
console.log(value); // 1 2 3
// 二度目以降は何も出力されない
for (let value of generator)
console.log(value);
// 二度目以降は nextメソッドの呼び出しでdone要素がfalseのまま
console.log(JSON.stringify(generator.next())); // {"done":true}
ジェネレータ関数はイテレーターを簡単に実装できて便利。
しかし通常、反復可能オブジェクトは再利用可能な方が使いやすい。
そのためジェネレータ関数を再利用可能にするには、ジェネレータ関数は単独で使うのではなくて他のオブジェクトの@@iteratorメソッドに指定する方が望ましい。
let myIterable={
[Symbol.iterator]: myGenerator
}
// myIterableから取得したイテレータはmyIterable自身ではない
console.log(myIterable[Symbol.iterator]() === myIterable); // false
// そしてmyIterableから取得したイテレータは常に新しいイテレータを返す。
for (let value of myIterable)
console.log(value); // 1 2 3
// 新しいイテレータを返すので再利用可能
for (let value of myIterable)
console.log(value); // 1 2 3
// myIterableオブジェクトから取得した直後のイテレータは常に最初の値を返す
console.log(JSON.stringify(myIterable[Symbol.iterator]().next())); // {"value":1,"done":false}
ジェネレーター関数内のreturn文の動作
ジェネレーター関数内でもreturn文を使う事ができる。
この場合に、return文はどうゆう働きをするかというと。
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
let it = generateSequence();
for (let v of it) {
console.log(v);
}
// 1
// 2
it = generateSequence();
let result = it.next();
while (!result.done) {
console.log(JSON.stringify(result));
result = it.next();
}
console.log(JSON.stringify(result));
// {"value":1,"done":false}
// {"value":2,"done":false}
// {"value":3,"done":true} <= return文によるnextメソッドの戻り値
すなわち、ChatGPTいわく。
ジェネレーター関数内部にreturn文がある場合、その関数は反復を強制的に終了します。
return文で指定された値が、ジェネレーター関数を呼び出したコード側に返されます。
この場合、ジェネレーター関数内部で未処理のyield式があっても、それらは無視されます。また、return文で返された値は、ジェネレーター関数自体の戻り値として扱われます。
つまり、ジェネレーター関数を呼び出すと、イテレータオブジェクトが返され、そのイテレータオブジェクトのnextメソッドを呼び出すことで、ジェネレーター関数の本体が実行され、最後にreturn文で指定された値が返されることになります。
yield*
yield*式を使うとジェネレーター関数内の反復可能な繰り返し動作に他の反復可能オブジェクトの繰り返し動作を挿入する事ができる。
yield* 式は別のジェネレーターや反復可能なオブジェクトに委任するために使用されます。
function* generatorSub() {
yield "a";
yield "b";
yield "c";
}
function* generatorMain() {
yield 1;
yield* generatorSub()
yield* [101, 102];
yield 2;
yield* "xyz";
yield 3;
}
let it = generatorMain();
for (let v of it) {
console.log(v);
}
// 1 a b c 101 102 2 x y z 3
yield*式をネスト
function* generatorSub() {
yield "a";
yield "b";
yield* generatorSubSub();
}
function* generatorSubSub() {
yield "c";
yield "d";
}
function* generatorMain() {
yield* generatorSub();
yield "e";
yield "f";
}
let it = generatorMain();
for (let v of it) {
console.log(v);
}
// a b c d e f
yield*とreturnとの組み合わせ
function* generatorSub() {
yield "a";
yield "b";
return "return value";
}
function* generatorMain() {
let return_value=yield* generatorSub();
yield 1;
yield 2;
yield return_value;
}
let it = generatorMain();
for (let v of it) {
console.log(v);
}
// a b 1 2 return value
ジェネレータ関数のクラスの継承関係
配列を例にとると、配列は組み込みのArrayクラスのインスタンスで、ArrayクラスのスーパークラスはObjectクラスになっている。
またArrayクラス自身は、ほとんどのクラスがそうであるようにFunctionクラスのインスタンスになっている。
これと同様に、ジェネレータ関数が返すオブジェクトはGeneratorクラスのインスタンスで、Objectクラスを継承している。
またGeneratorクラス自身はGeneratorFunctionクラスのインスタンスになっていて、
GeneratorFunctionクラスのスーパークラスはFunctionクラスになっている。
GeneratorクラスもGeneratorFunctionクラスも非公開のクラスで直接アクセスする事はできない。
Generatorクラス
ジェネレータ関数が返すオブジェクトはGeneratorクラスのインスタンスで、
この事を確認するために以下のようなコードを書いたが、 Generatorは非公開のオブジェクトのようでエラーになってしまった。
function* myGenerator() { }
let g = myGenerator();
// エラー Uncaught ReferenceError: Generator is not defined
console.log(g.constructor === Generator); // Generator
通常、インスタンスの属するクラスのクラス名はconstructorプロパティのnameプロパティから得る事ができるはずだが、空文字が返ってきてしまう。
function* myGenerator() { }
let o = myGenerator();
console.log(o.constructor.name); // 空文字
nextメソッドに引数を指定する
ジェネレーターオブジェクトにnextメソッドで値を渡す事もできる。
function* gen() {
let multi = 1;
for (let i = 1; ; i++) {
let recive_value = yield i * multi;
if (recive_value != undefined) {
multi = recive_value;
}
}
}
let it = gen();
console.log(it.next().value); // 1
console.log(it.next().value); // 2
// 次からは値が10倍される
console.log(it.next(10).value); // 30
console.log(it.next().value); // 40
nextメソッドに引数を指定しない場合はrecive_valueにはundefinedが代入されている。
Generatorのreturnメソッド
与えられた値を返してジェネレーター自身の処理を終了させる return(value) メソッド
GeneratorのreturnメソッドをChatGPTに説明してもらうと
Generatorのreturnメソッドは、ジェネレーターを終了させ、任意の値を返すメソッドです。
returnメソッドを呼び出すと、Generator関数内部のtry...finallyブロックが実行されます。
このtry...finallyブロックは、Generator関数内部で発生した例外を捕捉し、Generator関数を正しく終了するためのものです。tryブロック内部で、Generator関数内部でyieldされた最後の値が返されます。
この値は、Generatorの外部でreturnメソッドを呼び出したときに、その戻り値として返されます。
tryブロック内部でyieldされた値がない場合は、returnメソッドに渡された引数が戻り値として返されます。finallyブロック内部では、Generator関数の終了処理が行われます。
具体的には、Generator内部で使用されたリソースの解放などが行われます。returnメソッドは、Generator関数内部で任意のタイミングで呼び出すことができます。
また、returnメソッドを呼び出すと、Generator関数が強制的に終了するため、その後のyield文やthrow文は実行されません。
function* gen() {
for (let i = 1; ; i++) {
yield i;
}
}
let it = gen();
console.log(JSON.stringify(it.next())); // {"value":1,"done":false}
console.log(JSON.stringify(it.next())); // {"value":2,"done":false}
// イテレータを終了させる。valueにはreturnメソッドの引数が返される。
console.log(JSON.stringify(it.return("end"))); // {"value":"end","done":true}
// イテレータは終了していてvalueプロパティは存在しない。
console.log(JSON.stringify(it.next())); // {"done":true}
Generatorのthrowメソッド
ジェネレーターの throw() メソッドを呼び出して発生すべき例外値を渡すことで、ジェネレーターに例外を強制的に発生させることができます。
これにより、まるで停止中の yield が throw value 文に替わったかのように、ジェネレーターが停止した際の状況に応じて例外が発生します。
例外がジェネレーター内部で捕捉されない場合は、throw() を通してその例外が呼び出し元へと伝播し、その後 next() を呼び出した結果の done プロパティは true となります。
Generatorのthrowメソッドは、Generatorに例外を投げるために使用されます。
throwメソッドはGenerator内部で実行され、Generator関数の実行を中断し、例外を投げます。throwメソッドは、Generator内部で例外が発生した場合にも使用されます。
例えば、Generator内でtry-catch文を使用して例外を処理する場合、throwメソッドを使用してcatchブロックに制御を移すことができます。throwメソッドは、次のような形式で使用されます。
generator.throw(exception);
ここで、generatorはGeneratorオブジェクトを参照し、exceptionは投げたい例外オブジェクトです。
Generator内部で例外が発生した場合、Generatorはtry-catch文のcatchブロックに制御を移し、catchブロック内で投げられた例外を取得できます。また、Generator関数内でthrow文が実行された場合、その例外はGeneratorの外側に投げられます。
これにより、Generator関数を実行するコードで例外を処理することができます。
MDNに載っていたサンプルコードを引用すると。
function* gen() {
while(true) {
try {
yield 42;
} catch(e) {
console.log('Error caught!');
}
}
}
const g = gen();
g.next();
// { value: 42, done: false }
g.throw(new Error('Something went wrong'));
// "Error caught!"
GeneratorFunctionクラス
空のジェネレータ関数を定義して、その関数オブジェクトからクラスの継承関係をたどってみると
// 空のジェネレータ関数を定義
function* generator() { }
// ジェネレータ関数(オブジェクト)の継承関係をたどる
let o = generator;
let s = "";
o = o.__proto__;
while (o) {
if (s) {
s += "=>"
}
s += o.constructor.name;
o = o.__proto__;
}
console.log(s); // GeneratorFunction=>Function=>Object
ジェネレータ関数は非公開のGeneratorFunctionクラスのインスタンスになっていて、さらにGeneratorFunctionクラスはFunctionクラスから派生している事になる。
GeneratorFunctionクラスは非公開でグローバルオブジェクトではないが、ジェネレータ関数の__proto__
プロパティのconstructorプロパティより得る事ができる。
let GeneratorFunction = Object.getPrototypeOf(function* () { }).constructor;
GeneratorFunctionが得られれば、FunctionクラスのFunctionコンストラクターようにGeneratorFunctionからジェネレータ関数を生成することもできる。
let generator = new GeneratorFunction('a', 'yield a; yield a * 2;');
let it = generator(3);
for (let v of it) {
console.log(v); // 3 6
}
上記のコードの1行目は以下のジェネレータ関数の定義と等価である。
let generator = function* (a) {
yield a;
yield a * 2;
};