uhyohyo.net

JavaScript初級者から中級者になろう

十六章第七回 ジェネレータ2

このページの最終更新日:

今回は前回に引き続きジェネレータについて解説します。

基礎は前回済ませてしまったので、今回は少し細かい話になります。

Generatorのメソッド

まず、Generatorのメソッドについて紹介します。Generatorはジェンレータ関数を呼び出したときに返ってくるオブジェクトでしたね。

前回は最も基本的なメソッドであるnextを紹介しましたが、実はあと2つあります。ひとつはreturnでもうひとつはthrowです。この2つはnextに比べて使用頻度が圧倒的に低いですが、役に立つ場面があるかもしれないので紹介しておきます。

return

returnはジェネレータ関数の実行を強制終了するメソッドです。次の例を見てください。


function* gen(){
  yield 1;
  yield 10;
  yield 100;
}

var g = gen();
console.log(g.next()); // {value: 1, done: false}
console.log(g.return('foo')); // {value: "foo", done: true}
console.log(g.next()); // {value: undefined, done: true}

genは3回値をyieldする単純なジェネレータ関数です。genを呼び出して得たGeneratorオブジェクトに対して1度next()を呼ぶと、1回目のyieldから1が返りました。ここまでは通常通りですね。

次にreturnを呼びました。すると、返り値はnextと同様のオブジェクトですがdoneがtrueになっています。また、valueはreturnの引数に与えた値になっています。つまり、returnを呼び出すことでジェネレータが強制的に終了状態になり、その時返ってくる値もreturnの引数で指定することができるのです。

この時点でジェネレータは終了状態になったため、nextメソッドを呼んでも値は返らず、doneがtrueとなっています。

これでreturnメソッドの動作は分かりましたが、なぜreturnという名前なのでしょうか。実は、これはreturn文と関係があります。

上の例で1回目のg.next()が呼ばれたあと、ジェネレータ関数は1回目のyieldの評価で止まっています。


function* gen(){
  yield 1; // ←この文の評価で止まっている
  yield 10;
  yield 100;
}

評価で止まっているというのは、yieldは(nextメソッドで渡された)返り値を返すかもしれないので、関数の中から見るとyieldの値を計算しようとしたところで止まっているということです。次のnextメソッドが呼ばれると、関数の中から見るとyieldから値が返ったように見えます。

この状態でnextではなくreturnメソッドを呼ぶと、直感的には次のようなことが起こります。


function* gen(){
  return 'foo'; // これはyieldじゃなくて実はreturn文でした!!!!!!
  yield 10;
  yield 100;
}

returnメソッドは、ジェネレータ関数の中で止まっているyieldをreturn文に書き換えてしまうのです。(ただし、厳密には本当に構文が書き換わっているわけではありません。yieldは式中で使えるのに対しreturnは式中に書けないから当たり前ですが。)

こうなると、ジェネレータ関数が実行しているのは実はreturn文だったのでジェネレータ関数の実行はもう終了しなければなりません。そのときの結果がreturnメソッドの返り値として現れます。結局やっていることはジェネレータ関数を強制終了しているだけですが、その実態はこのようになっているためこのメソッドはreturnという名前なのです。

throw

となると、もうひとつのメソッドthrowはどういうメソッドなのか想像がつきますね。そう、throw文です。throwメソッドを呼び出した場合、当該のyieldをthrow文で書き換えます。もっと簡単に言えば、yieldから例外が発生します。発生する例外は、throw文の引数で指定します。


function* gen(){
  yield 1;
  yield 10;
  yield 100;
}

var g = gen();
console.log(g.next()); // {value: 1, done: false}
console.log(g.throw(new Error('Hey!'))); // エラー

こうすると、2回目のconsole.logは行われません。なぜなら、g.throwメソッドを実行した時点でジェネレータ関数内で例外が発生するからです。つまり、これはgenの中が次のように書き換わったのに相当します。


function* gen(){
  throw new Error('Hey!');
  yield 10;
  yield 100;
}

上の例では、例外が発生してプログラムが止まってしまいました。しかし、例外はtry-catch文でキャッチすることができるのでした。もちろん、yieldから発生した例外もキャッチできます。


function* gen(){
  try {
    yield 1;
    yield 10;
    yield 100;
  } catch(e) {
    yield -42;
  }
}

var g = gen();
console.log(g.next()); // {value: 1, done: false}
console.log(g.throw(new Error('Hey!'))); // {value: -42, done: false}

こうすると、throwメソッドを呼んだ時点でジェネレータ関数のyield 1;から例外が発生します。しかしジェネレータ関数はそれをキャッチして処理を続けます。そうするとyield -42;にたどり着いて値をyieldします。この結果がthrowメソッドの返り値として現れています。このように、ジェネレータ関数が例外を処理できた場合、次のyield(またはreturn)から発生した結果がthrowの返り値となります。

余談ですが、例外が発生してキャッチできなかったイテレータは終了状態となります。


function* gen(){
  throw new Error('gya---');
  yield 1;
}

var g = gen();
try {
  console.log(g.next());
} catch(e){
  console.error(e); // Error: gya---
  console.log(g.next()); // {value: undefined, done: true}
}

yield*式

実は、yield式の仲間としてyield*式というのがあります。

結論から述べると、これはiterableを渡すとイテレータから出力された値を全部yieldします

iterableの代表例は配列なので、次の例で試してみましょう。


function* gen(){
  console.log(yield* [1, 2, 3]);
}

var g = gen();
console.log(g.next()); // {value: 1, done: false}
console.log(g.next()); // {value: 2, done: false}
console.log(g.next()); // {value: 3, done: false}
console.log(g.next()); // {value: undefined, done: true}

yield*により、1,2,3が順番にyieldされたことが分かります。

yield*式ということはこれにも返り値があります。yield*の返り値はyieldとは異なり、外からの入力ではありません。返り値はイテレータがdone:trueを返したときのvalueの値となります。

以前に見たように、普通のイテレータは終了時(done:trueのとき)はもう値が出てこないのでvalueの意味はありません。ただ、ものによってはdone:trueを返すときでもvalueを伴っている場合があります。そのようなイテレータの代表例はまさにジェネレータオブジェクトですね。ジェネレータ関数がreturnしたとき、done:trueとなりつつ返り値の値がvalueとして返ってきます。これを試してみたのが次の例です。


function* gen1(){
  yield* [1, 2, 3];
  return 'foo';
}
function* gen2(){
  const result = yield* gen1();
  console.log('resultは', result);
}

var g = gen2();
console.log(g.next()); // {value: 1, done: false}
console.log(g.next()); // {value: 2, done: false}
console.log(g.next()); // {value: 3, done: false}
// ここでresultは fooと表示
console.log(g.next()); // {value: undefined, done: true}

このようにジェネレータ関数の中から別のジェネレータ関数を呼び出すというパターンは結構使いどころがあるかもしれません。

細かい話

以上で紹介したい機能は終わりです。ここからは細々とした話をしていきます。

ジェネレータ関数式

今までの例ではfunctionによる関数宣言と同様にfunction*によりジェネレータ関数を宣言していました。普通の関数と同様に、次のように関数式の形でもfunction*は使えます。


var gen = function*(){
  yield* [1, 2, 3];
};

ジェネレータ関数内でのthis

実は、ジェネレータ関数がメソッドとして呼ばれた場合はthisの値も使えます。


var obj = {
  foo: 100,
  bar: 200,
  gen: function*(){
    yield this.foo;
    yield this.bar;
  },
};

var g = obj.gen();
console.log(g.next()); // {value: 100, done: false}
console.log(g.next()); // {value: 200, done: false}

ジェネレータ関数はコンストラクタではない

第九章で紹介したように、JavaScriptでは関数は自動的にコンストラクタとなり、newで呼び出すことができるのでした(もっとも、コンストラクタを意図して作られたわけではない関数をnewしてもあまり嬉しいことはありませんが)。

一方、ジェネレータ関数はコンストラクタではないことになっています。よって、普通に呼び出すことはできても、newで呼び出すことはできません。

function.sent

最後にfunction.sentの話をして終わりにします。ただしこの機能はES2015の範囲ではなく、それどころかまだ正式なJavaScriptの機能ではありません。2017年10月現在、Stage 2のProposalです。つまり機能が提案段階であり、まだ確定していないということです。そのため、ブラウザで実行しようとしてもまだ対応していないと思います。まあジェネレータに関係する話ですので、こういうのもあるんだという参考程度に知っておいてもらえればよいでしょう。

function.sentはメタプロパティの一種です。メタプロパティとは、プロパティのような構文だけどプロパティではないもののことです。実際、これはfunctionというオブジェクトのsentというプロパティを参照しているように見えますが、これは不可能です。なぜなら、functionはキーワードなので、functionと書いてもfunctionという名前の変数であるとは認識されないからです。あくまでfunction.sentでひとまとまりの構文です。

function.sentはジェネレータ関数の中で使えるメタプロパティです。これは、直近のnextメソッドに引数として渡された値が入っています。

前回の説明では、これはyieldの返り値として取得できるものでした。これを取得する別の方法ということになります。

これの必要性を理解するためには、前回のこのサンプルを思い出してください。


function* sum(){
  let acc = 0;
  while (true){
    const x = yield acc;
    acc += x;
  }
}

var g = sum();
g.next(); // 実行を開始

console.log(g.next(1)); // {value: 1, done: false}
console.log(g.next(2)); // {value: 3, done: false}
console.log(g.next(3)); // {value: 6, done: false}
console.log(g.next(4)); // {value: 10, done: false}

このジェネレータは、nextメソッドに値を渡すとそれが足されていくというサンプルでした。問題は、1回目のg.next()だけ例外的な扱いになっているとです。その理由は、最初のnextはジェネレータ関数の実行を最初から開始するのであり、yieldで止まっていたところを再開するわけではないので、最初のnextメソッドに引数として渡された値を取得する方法が無かったからです。function.sentメタプロパティを用いることで1回目の実行でもsentに渡された値を取得することができ、1回目と2回目以降を同等に扱うことができます。今回の例の場合、具合的には次のようにします。


function* sum(){
  let acc = 0;
  while (true){
    const x = function.sent;
    acc += x;
    yield acc;
  }
}

var g = sum();

console.log(g.next(1)); // {value: 1, done: false}
console.log(g.next(2)); // {value: 3, done: false}
console.log(g.next(3)); // {value: 6, done: false}
console.log(g.next(4)); // {value: 10, done: false}