反復処理はプログラムの基本です。プログラミング言語がどの程度反復処理を補助してくれるかによって、プログラミングの快適性は大きく変わります。
JavaScriptは反復処理に関しては、どちらかというと遅れている言語でした。ですが、IteratorとGeneratorの導入によって、それが変わろうとしています。
Iterator
ItreableとIterator
JavaScriptにおいて、繰り返し可能なオブジェクトのことをIterable(反復可能)と言います。Iterableの代表例としてはArrayやMapがあります。これらは複数の値を持つので、反復可能と言えます。
IterableなオブジェクトはIterator(反復子)を持ちます。IteratorはIterable[Symbol.iterator]()で取得可能です。
const array = [1, 2, 3];
const iterator = array[Symbol.iterator]();
Iteratorは唯一のメソッドnext()を持ちます。next()を実行することで、Iterable内の次の値を取得することが可能です。
const array = [1, 2, 3];
const iterator = array[Symbol.iterator]();
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3
next()で得られる値は、{value: 次の値, done: 終了したか否か}となるオブジェクトです。valueの値をチェックすることで、Iterableの値を取得することができます。また、doneの値を調べることで反復が終了したか否かがbool値でわかります。
つまり、上記のコードはループを用いて次のように書き直すことができます。
const array = [1, 2, 3];
const iterator = array[Symbol.iterator]();
let next = iterator.next();
while(!next.done) {
console.log(next.value); // 1 2 3
next = iterator.next();
}
Iteratorを使うことで、繰り返し処理をスマートに書くことができるのです。
ですが、これではまだ煩雑です。そこで、javaScriptではIteratorの反復をより簡単に書く方法が用意されています。
for-of文
for-of文はIterableを扱う最も簡単な方法です。言葉で説明するより、先にコードを見た方がわかりやすいでしょう:
const array = [1, 2, 3];
for(const val of array) {
console.log(val); // 1 2 3
}
for-of文は、前述のwhile文での処理とほぼ同じことを自動的に実行してくれます。つまり、IterableからIteratorを取り出し、iterator.next()を呼び出し、doneがfalseの間、処理を繰り返します。for-of文を使うことで、繰り返し処理を簡単に書くことができます。
IteratorとIterableの自作
IteratorやIterableは自作することも可能です。Iteratorの自作の方法は簡単で、{value, done}を返すnext()メソッドを実装するだけです。Iterableも同じく[Symbol.iterator]()を実装するだけです。
例えばrange()関数を実装することを考えてみましょう。IteratorとIterableを作成する関数は、それぞれ以下のようになります。
function createRangeIterator(end) {
let currentValue = 0;
const iterator = new Object();
// {value, done}を返すnext()メソッドを実装する。
// これでIteratorが出来上がる。
iterator.next = () => {
const value = currentValue++;
const done = currentValue > end;
const result = done ? {done: done} : {value: value, done: done};
return result;
};
return iterator;
}
function range(end) {
const iterable = new Object();
// 注意! Iterator自身ではなく
// Iteratorを返す「関数」を用意する。
iterable[Symbol.iterator] = () => createRangeIterator(end);
return iterable;
}
for(const val of range(3)) {
console.log(val); // 0 1 2
}
これでrangeメソッドが出来上がりました。Iteratorの仕組みは単純なので、簡単に自作することができます。
Generator
Generator関数とyield return
Iteratorの仕組み自体は単純とはいえ、実際に実装するとなるとなかなかの行数になります。もちろんこのコードをすらすら読み書きできる優れたプログラマもいるのでしょうが、一般には難しいことです。
そこでJavaScriptではGeneratorという仕組みが用意されています。GeneratorはIteratorやIterableを非常に簡単に扱う手段を提供します。Generatorを使うことで、簡単なコードで自動的に反復処理を行うことができます。
Generatorを利用するには、functionの代わりにfunction*を使います。そしてreturnの代わりにyieldを使用します。これだけでGeneratorが完成します。ここで、function*で定義された関数のことを、Generator関数と言います。
function* generator() {
yield 1;
yield 2;
yield 3;
}
for(const val of generator()) {
console.log(val); // 1 2 3
}
Generator関数は、Generatorオブジェクトを返します。GeneratorオブジェクトはIterableかつIteratorとなるオブジェクトです。Iteratorとしてgenerator.next().valueなどとしてもいいですし、Iterableとしてfor-ofを使って値を取り出すなど、自由に使用できます。
Generator関数はnext()が呼び出されると、yieldが出てくるたびに一度停止し、yieldした値を返します。そしてまたnext()が呼び出されると、関数の実行を再開して次のyieldまで進めます。Generatorはこれを繰り返すことでIteratorのようにふるまいます。
Iterableをyieldするyield*
また、yieldの代わりにyield*を使用することで、Iterableの中身をひとつずつ自動的にyieldすることができます。
function* generator() {
yield* [1, 2, 3];
}
for(const val of generator()) {
console.log(val); // 1 2 3
}
コンテキストの保存
yieldおよびyield*はその関数のコンテキストを保存します。つまり、関数内における変数の値などはそのまま保持されるということです。具体的な例で言うと、以下のようなGeneratorを書くことができるということです:
function* generator() {
for(let i=0; i< 3; i++) {
yield i;
}
}
for(const val of generator()) {
console.log(val); // 0 1 2
}
これを利用することで、例えば先ほどのrange関数を以下のように簡単に書くことができます:
function* range(end) {
for(let i=0; i < end; i++) {
yield i;
}
}
for(const val of range(3)) {
console.log(val); // 0 1 2
}
ずいぶんと簡単に書くことができました。
まとめ
- 反復可能なオブジェクトはIterable
- IterableなオブジェクトはIteratorを持つ
- for-of文を使うことで簡単にIterableを反復できる
- IteratorとIterableは自作可能
- Generatorを利用することで簡単にIterableを作ることが可能
- GeneratorはIterableかつIterator
- yieldで次の値を返すことができる
- yield*でIterableの値をそれぞれ返すことができる
- yieldで関数を一時的に抜けてもコンテキストは保存される