Aug 07, 2006

JavaScript 1.7のyield文ってなんじゃらほ

JavaScript 1.7 の yield が凄すぎる件についてを見てもyieldってそもそも何なのかちいとも分かっとらんかったのでそこから調べてみた。

yieldはreturnの仲間?

そもそもこのyield文というのは、JavaScriptと同じくスクリプト言語のPythonから持ち込まれた仕組みらしい。ジェネレータとは何ぞやで読める例を見た感じでは、どうやら、関数内での使い方としてはreturn文の仲間というかそういうもののように読める。

return文は知ってのとおり、関数の実行をその場で終了して、引数として与えられた変数の値を返すというもの。「yield文が登場する関数」の中から見たら、yieldもそれと似たような動作をする、すなわち「yield文に引数として渡した変数の値が返される」ということ。

yield文がreturn文と違うのは、yield文が登場する関数の外から見たときの挙動だ。

return文の場合、return文が含まれる関数を実行すると、return文に渡された変数の値が返ってくる。これは分かりやすい。


function test() {
  var value = 'hoge';
  return value;
}

alert(test()); // "hoge"と表示される

yield文の場合、yield文が含まれる関数を実行しても、yield文に渡された変数の値は返ってこない。じゃあ何が返ってくるかというと、「ジェネレータ」と呼ばれる特殊なオブジェクトが返ってくる。


function test() {
  var value = 'hoge';
  yield value;
}

alert(test()); // "[object Generator]"と表示される

ジェネレータ(「ジェネレータ・イテレータ」が正式な名前らしい)とはnext()というメソッドを持つオブジェクトの型で、イテレータの一種だ。

イテレータって何よ

イテレータとは、内部に複数の要素を持つ配列と「どの要素にフォーカスしているか」の情報を保持しているようなオブジェクトで、next()メソッドを実行するたびに、「現在フォーカスしている要素」を返しつつ内部のカウンタを一つ進める、というふうな振る舞いを見せるオブジェクトのデザインパターンの一種だ。DOM3 XPathで型としてXPathResult.ORDERED_NODE_ITERATOR_TYPEを渡したときのXPathResultの挙動なんかもまさにそれにあたるし、例えば以下のようなものもイテレータと言える。


var iterator = {
      next     : function() {
        if (this.count >= this.elements.length) {
          throw 'もう全部見たよ!';
        }
        return this.elements[this.count++];
      },
      count    : 0,
      elements : [
        'hoge', 'hage', 'foobar'
      ]
    };

alert(iterator.next());
alert(iterator.next());
alert(iterator.next());

ちなみにイテレータで最後の要素まで参照し終えた後でさらに次の要素を参照すると、普通はエラーになる。上の例でもthrow文でそれを再現してみた。まあ、わざわざエラー処理を書かなくても配列の未定義の要素へのアクセスが発生した段階でエラーになるわけだけど。

(なお、上記の例では配列を内部に保持しているけれども、イテレータというデザインパターン自体は、配列を使う必要があるというものではない。というより、「長さ(要素の数)」が決まっていないといけない「配列」と違って、「全体の数は分からないけど該当する要素の条件は分かっているので、その条件にマッチするものを一つ一つ順番に渡り歩いていく」ということが可能なのがイテレータだ。そもそも、配列とループを使った処理といえば、一つ一つ順番に配列の要素を処理していって最後の要素にきたら終了するというものがほとんどで、必ずしもループ開始の時点で配列の全体の数を把握している必要はないのだから、イテレータとして実装した方が効率は良いというわけ。)

ジェネレータ

内部でyield文を使った関数は、ただの関数ではなくてジェネレータを生成するための特殊な関数(ファクトリー)という扱いになる。この関数を実行すると、帰り値はジェネレータオブジェクトとなる。よって、実際に使うときは以下のような感じになる。


function test() {
  var value = 'hoge';
  yield value;
}

var generator = test();
alert(generator.next()); // "hoge"と表示される

next()なんてメソッドの定義はどこにも書いた覚えがないのに、勝手にそういうものが生成される。強いて言うなら、yield文を含んだ関数の定義内容がそのままnext()メソッドの内容になる、みたいな感じ?

「イテレータの例」で書いたようなカウンタだとか内部的な配列だとかの定義をあれこれ用意しなくても、JavaScriptエンジンの方で勝手にイテレータちっくな挙動にしてくれる……というのがたぶんジェネレータの便利なところの一つだと思う。このおかげで、例えばそのイテレータをカウンタと配列を使って実現しているのであれば、カウンタや内部の配列を外部から操作される心配もなくなる(例えば、上記の例の実行中にiterator.count = 0;とかされたら処理が狂うけれども、ジェネレータではカウンタも要素も不可視なので、こういう問題は起こらない……はず)。

でも、イテレータは配列とカウンタを使わないで実装することもできる。そこでyield文が活躍することになる。

yield文の面白いところ

ジェネレータを実際に動かすときに、yieldとreturnのもう一つの違いがはっきり出てくる。return文はそこで関数の実行を強制的に終了させてしまう。それに対してyield文は、yield文が実行されたとき、渡された引数の値を返すと同時に、ビデオの「一時停止」ボタンを押したようにその行で処理を一時停止する。そして、次にnext()メソッドが呼ばれたときに、その次の文から処理を再開する。(そして、次のyield文に辿り着いた所でまた一時停止して、値を返す。)一時停止してから再開するまでの間、処理の状態や変数の内容も完全に保持される。関数にとっての「一時停止」文としても作用するというわけだ。

だから極端な話、こんなこともできる。


function test() {
  var i = 0;
  yield 'hoge ' + (i++);
  yield 'hage ' + (i++);
  yield 'foobar ' + (i++);
}

var generator = test();
alert(generator.next()); // "hoge 0"
alert(generator.next()); // "hage 1"
alert(generator.next()); // "foobar 2"
alert(generator.next()); // これはエラーになる

yieldのこの機能とループと組み合わせると、無限数列を求める処理なんかを書くことができる。以下はフィボナッチ数列を求める例。


function test() {
  var a = 0;
  var b = 1;
  while (true) {
    yield a;
    var temp = b;
    b = a + b;
    a = temp;
  }
}

var generator = test();
alert(generator.next()); // "0"
alert(generator.next()); // "1"
alert(generator.next()); // "1"
alert(generator.next()); // "2"
alert(generator.next()); // "3"
alert(generator.next()); // "5"
alert(generator.next()); // "8"
alert(generator.next()); // "13"

このように単純な例だと、ジェネレータを使わずに以下のように書いても大して手間は変わらない。


var a = 0;
var b = 1;

function test() {
  var tempA = a;
  var tempB = b;
  b = a + b;
  a = tempB;
  return tempA;
}

alert(test()); // "0"
alert(test()); // "1"
alert(test()); // "1"
alert(test()); // "2"
alert(test()); // "3"
alert(test()); // "5"
alert(test()); // "8"
alert(test()); // "13"

しかし、IT戦記のエントリで挙げられているアニメーションの例のように幾つものカウンタが回る複雑な処理になってくると、ジェネレータでyield文を使ってその都度「一時停止」できたほうが便利な場合も出てくる。

あと、amachang氏が仰っているように、yieldとジェネレータの機能の副産物として、これらを使うと処理のスレッド化(適当なところで処理を一時停止して、他の処理に制御を移し、そっちの処理もまた適当なところで一時停止して、また一番最初の処理に戻ってくる、という感じのこと)もできる。

……というのが、ジェネレータの使いどころなんだと思う。僕は頭が悪いんで、有効に活用できる場合というのを自分ではさーっぱり思いつかないんだけど。なので、せめて、頭のいい人がこの解説みたいな文章を見てyieldの有効な使い道をたくさん考えてくれることを期待します。

エントリを編集します。

wikieditish message: Ready to edit this entry.











拡張機能