uhyohyo.net

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

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

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

今回はジェネレータgenerator)を解説します。これは、ざっくり言うと途中で抜けたりまた入ったりできる関数です。

今まで、関数が実行されると必ずその関数は最後まで(またはreturn文にたどり着くまで)実行されました。今回紹介するジェネレータ関数generator function)を使うとそうではない関数を作ることができます。

function*

では、例を見てみましょう。


// ジェネレータ関数を定義
function* gen(x){
  console.log(x);
  yield;
  console.log(x*2);
  yield;
  console.log(x*3);
}

var g = gen(10);
console.log("1回目");
g.next();
console.log("2回目");
g.next();
console.log("3回目");
g.next();

この例には2つ新しい文法が出てきました。ひとつは、function*による関数宣言です。これは関数宣言のようですが、functionキーワードのあとに*(アスタリスク)が付いています。このように宣言された関数はジェネレータ関数となります。

もうひとつはyield式です。yield式はジェネレータ関数の中でのみ使用できる構文で、これは関数から一旦抜けることを意味します。

では、上の例を実行するとどうなるでしょうか。以下のようにコンソールにログが表示されるはずです。


1回目
10
2回目
20
3回目
30

細かいことはこれから説明しますが、関数の外のconsole.logと関数の中のconsole.logが交互に実行されており、関数を出たり入ったりしていることが分かると思います。

ジェネレータオブジェクト

実は、ジェネレータ関数(上の例だとgen)を呼び出しただけでは関数は実行されません。代わりに、関数の実行を制御するためのオブジェクトであるGeneratorオブジェクトが生成されて返されます。

Generatorはnextメソッドを持ちます。nextメソッドを呼び出すことで初めてgenが実行されます。なので、ジェネレータ関数の基本的な使い方は、まずGeneratorオブジェクトを取得してnextメソッドで関数を実行するということになります。

ただし、先に述べたようにジェネレータ関数の実行はyield式で止まります。つまり、1回のnext()では次のyield式までしか行かないのです(または、yieldがもう無い場合は関数の最後まで進みます)。上のサンプルでは、nextメソッドを何回も呼び出すことで関数の実行を進めています。

これは単純な例でしたが、適切にyieldが挿入されたジェネレータ関数は、このようにGeneratorオブジェクトを通じて実行を制御できます。

関数とのコミュニケーション

従来、関数との間でデータをやり取りする方法は、引数で関数にデータを渡し、戻り値で関数からデータをもらうだけでした。これは、一度関数に入ったら関数が終了するまで関数から出てこないので仕方がないですね。

一方、ジェネレータ関数は関数から出たり入ったりできるという特徴があります。実は、そのときにデータの受け渡しが可能なのです。ひとつずつ紹介します。

関数から出るときにデータを渡すには、yield式にデータを渡します。return文と同じ感じですね。


// ジェネレータ関数を定義
function* gen(x){
  yield x;
  yield x*2;
  yield x*3;
}

var g = gen(10);
console.log("1回目", g.next());
console.log("2回目", g.next());
console.log("3回目", g.next());

yield式が実行されると関数が一時停止します(すなわち、関数を呼び出したg.next()から返ってきます)。このとき、yield式に渡された値はnext()の返り値に現れます。ただし、このサンプルを実際に実行してみると分かるように、nextの返り値は次のようなオブジェクトです。


{
  value: 10,
  done: false,
}

すなわち、yield式により関数から返された値はnext()の返り値のオブジェクトのvalueプロパティに入っています。また、もし関数が終了した(関数の最後に到達したかreturn文が実行された)場合はvalueプロパティにはその返り値が入っており、その場合doneがtrueとなっています。

記憶力がいい方はあることに気づいたと思いますが、その話は後ですることにします。

まず関数から抜けるときに関数から値をもらう方法を学びました。となると、次は逆ですね。関数に再び入るときに関数に値を渡すには次のようにします。


// ジェネレータ関数を定義
function* gen(x){
  console.log('x:', x);
  const y = yield;
  console.log('y:', y);
  const z =yield;
  console.log('z:', z);
}

var g = gen(10);
g.next();
g.next(100);
g.next(1000);

関数に入るときにg.next()の引数で値を渡します。すると、その値はyield式の返り値として関数の中で取得できます。執拗にyieldをyieldと呼んでいたのは、このようにyieldは式中で使うことができるからです。

このサンプルを実行すると次のようにログが出ます。これはなぜなのか考えてみてください。


x: 10
y: 100
z: 1000

特に、1回目のnextの呼び出しだけ引数がないことに注意してください。これは、1回目のnext()では関数の最初から実行が始まるので対応するyield文がないからです。最初の実行は1回目のyield文で止まり、2回目のnextに渡された引数が1回目のyield文の返り値となります。

この仕様は不自然ですが、そうなっている以上仕方ありません。1回目のnext()は関数の実行開始用であると割り切るのもよいでしょう。

まとめとして、両方向のコミュニケーション方法を組み合わせた例を出しておきます。


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}

このジェネレータ関数は渡された数値を全部足していき、そのたびに現在までの合計を返すものです。この関数はwhileの無限ループの中でyield文を実行しているので、無限にyieldし続けることができます。

ジェネレータとイテレータ

さっき記憶力かどうのと言いましたが、それはイテレータを覚えていますかということです。

Generatorオブジェクトはnextメソッドを持ち、その返り値はvalueとdoneプロパティを持つオブジェクトでした。これはイテレータの特徴を満たしていますね。ただしイテレータのnextメソッドは一般には引数を持ちませんから、ジェネレータのほうが高機能です。とはいえ、ということは、ジェネレータオブジェクトはイテレータとしても使えるということです。

さらに、実はGeneratorオブジェクトは自身をイテレータとするiterableでもあります。言い換えると、自分自身を返すSymbol.iteratorプロパティを持つということですね。この事実を用いると、iterableを簡単に作るためにジェネレータ関数を使うことができます。例えば、こんな例はなかなか実用的ではないでしょうか。


// startからendまでの整数を順番に発生させるジェネレータ関数
function* ran(start, end){
  for (let currentValue = start; currentValue <= end; currentValue++){
    yield currentValue;
  }
}

for (const i of ran(5, 10)){
  console.log(i);
}

この例ではran関数が返すジェネレータオブジェクトをイテレータとして使っています。つまり、ranは2つの引数を受け取り、その間の整数を順に列挙するイテレータを返すものとして使うことができます。

以上がジェネレータの基礎です。まだ細かい話が色々あるのですが、それは次回にしましょう。