実例とともに学ぶECMAScript 2015 〜Generator〜

hiroqn
149

Introduction

この記事は最近JavaScriptに入ったGeneratorと呼ばれる機能について知りたい、もっと詳しく知りたいという方をターゲットとしています。

今回はECMAScript 2015に入った機能のgeneratorについて解説していきたいと思います。
generatorはNode界隈では2014年ごろから非同期処理を同期的に書くことができるツールとしてよく使われていました。
最近ではasync/awaitをよく耳にすることがあると思いますが、async/awaitgeneratorを使ったものに変換可能なので挙動としては近いものと考えてもらって大丈夫です。

What is generator

前置き

では、generatorとはなんでしょうか?「実はgeneratorIteratorを返す関数なのです!」これが一番説明しやすいので、これをベースに今回は解説していきたいと思います。

というわけでまずIteratorの説明をまだしていないので今からします。

Iterator

Iteratorはよく反復可能オブジェクトと訳されます。少なくともMDNはそうなっていました。

反復可能オブジェクト これは直感的じゃない表現かも知れません。比較的分かりやすい言葉で言うとfor文などの繰り返しを扱うもの、という表現がいいかもしれません。

もし「IteartorとはどんなObjectか」と質問されたら、以下のように答えます。

nextメソッドが存在し、そのメソッドの返り値はvalueプロパティとdoneプロパティを少なくとも持ったオブジェクトです。valueは反復した結果、doneはこれ以上反復できないかを表す真偽値です。

箇条書きで書くとすると、

Iteratorとは、以下の条件をみたすオブジェクトのことです。

  • next メソッドを持っています。
  • nextvalue, done プロパティを持つオブジェクトを返します。valueは反復処理における結果、doneは反復が終了したかです。

型を知りたいという方は、TypeScriptの型定義ファイルを参照してみましょう。

interface IteratorResult<T> {
    done: boolean;
    value?: T;
}

interface Iterator<T> {
    next(value?: any): IteratorResult<T>;
    return?(value?: any): IteratorResult<T>;
    throw?(e?: any): IteratorResult<T>;
}

となっています。

型を見てもらうとnextに値が渡せることに気づかれる方もいると思いますが、今回はその機能やreturnthrowは使いません。

example

試しにコードを書いてみましょう。

/**
 * @param n {number}
 * @return {Iterator<number>}
 */
function range(n) {
  var value = 0;
  return {
    next() {
      if (value < n) {
        const result = {value, done: false};
        value += 1;
        return result;
      }
      return {value: undefined, done: true};
    }
  };
}

実行例

const iter = range(10);

console.log(iter.next());// {value: 0, done: false}
console.log(iter.next());// {value: 1, done: false }
console.log(iter.next());// {value: 2, done: false}

このようにnextを呼ぶと次の値が来る状態が作れました。どうでしょうか、反復という感じがするでしょうか?

Iterable

Iterableというものがあります。今までIteratorについて説明してきたのもIterableを説明するためです。generatorについての解説はもう少し後になります。

IterableはあるオブジェクトのSymbol.iteratorプロパティにIteratorを返す関数が設定されているものを指します。SymbolはECMAScript 2015の機能の1つで、string以外をプロパティに持つことができるようになる機能です。一応TypeScriptの型定義ファイルを参照してみましょう。

interface Iterable<T> {
    [Symbol.iterator](): Iterator<T>;
}

つまり、Iterableは繰り返し処理をすることができるという機能が実装されているオブジェクトということです。この機能は何のために実装されているのでしょうか? 実は、IterableはECMAScript 2015のさまざまな機能において実装に用いられています。

for of

Iterableなものはfor文のように中身を1つ1つ取り出しながら処理を書くことができます。

for(const i of range(10)) {
  console.log(i)
}

実行してもらうと分かりますがこれはエラーになります。なぜならrangeの返すものはIteratorであって、Iterableではないからです。というわけで、IterableIteratorを返すようにrangeを再実装してみましょう。

/**
 * @param n {number}
 * @return {Iterator<number>}
 */
function range(n) {
  var value = 0;
  return {
    next() {
      if (value < n) {
        const result = {value, done: false};
        value += 1;
        return result;
      }
      return {value: undefined, done: true};
    },
    [Symbol.iterator]() {
      return this;
    }
  };
}

Iterator自身を返してあげればいいので簡単ですね。上のfor ofの例を実行すると0〜9が出力されると思います。

Array.from

Iterableは複数の値を扱うので複数の値をArrayにすることができます。

console.log(Array.from(range(10)))

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]が表示されると思います。

spread operator

展開演算子と呼ばれるものがあります。これはIterableなものを展開することができます。例を見ていただいたほうが分かりやすいかと思います。

const array = [...range(5)];

これは[0, 1, 2, 3, 4]となりArray.fromと同じように感じますがもう少し便利です。

const array = [...range(5), ...range(5), ...range(5)];
// [0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4]
const array = [666, ...range(5), 666];
// [666, 0, 1, 2, 3, 4, 666]

[]このリテラル中ではArrayになりますが、関数適用の()の中ではまた別の挙動を見せます。

function f(a, b, c, d, f) {
  console.log(a, b, c, d, f)
}

を定義したあとf(...range(5))を実行してみてください、a,b,c,d,f0,1,2,3,4が入っていると思います。

rest

今度は分割代入と呼ばれる機能の一部として、...を使うことができます。分割代入は他にも機能がありますが、今回は...を使うところを紹介します。

const [a, b, c] = range(5); //aに0,bに1,cに2が入ります。

分割代入はこのようにIterableなものの中身を取り出し変数に代入することができます。

const [x, y, ...z] = range(5);

この時どういう結果になるでしょうか? 実行してもらうとx0,y1,z[2,3,4]Arrayが入っているのが確認できると思います。

また

function g(x, y, ...z) {}

という関数があった場合、g(...range(5))とすると同様にx,y,zに上と同じ値が入ります。これはargumentsIterableであることとも関連しています。

built in Iterable

StringArrayTypedArrayMapSetや先ほどあったargumentsなどはすでにIterableです。また、後述するgeneratorで作ったIteratorIterableになっています。

なので以下の様なコードは同様に動きます。

var [i, j, k] = [1, 2, 3];
[i, j, k] = 'abc'; // i,j,kはa, b, c
const iterator = 'xyz!'[Symbol.iterator]();
iterator.next();//{value: 'x', done: false}

Generator

ようやくgeneratorです。Iteratorをもっと簡単に作りたい気持ちになってきましたか? 先ほどのrangegeneratorを使うと、こう書けるようになります。

function *

function * range(n) {
  let i = 0;
  while (i < n) {
    yield i;
    i += 1;
  }
}

generatorを書くときはfunction * () {}を使います。このように書くとyieldというキーワードがそのスコープ内で使えるようになり、function * () {}で書いたgenerator関数は常にIteratorを返します。そしてyield xxxxxxIteratorvalueになります。なので、今回書いたrangeも今までのrangeとほとんど同様に動く関数になります。

generatorは慣れることが大切と思いますので、今回は詳細な挙動を確認することはしません。

yield*

yieldではなくyield*というキーワードがあります。これはIterablegeneratorの中で展開することができるものです。つまり yield* xxxxxxには必ずIterableが来ないといけません。

おそらくこれでは挙動が分かりづらいと思うので、例を上げていきます。

先ほどのrangeの範囲の値を指定した回数繰り返すIteratorを返すような関数があるとします。関数名はrepeatRange(n, c)にしましょう。rangeRepeat(5, 3)とすると0〜4が3回繰り返されるようなIteratorが得られるという仕様です。

function * rangeRepeat(n, c) {
  while (c > 0) {
    yield* range(n);
    c -= 1;
  }
}

こうです。意外と簡単ですね。

またこの機能を使うと幾つかの関数をgeneratorを使ったまま再帰で書くことができます。

function range(n) {
  return function * f(i) {
    if (i < n) {
      yield i;
      yield* f(i + 1)
    }
  }(0);
}

今回のケースだとそこまで再帰で書くメリットは薄いですが、非常に効果的に書くことができる時もあります。

tips

function * f(n) {
  let i = 0;
  function g() {
    yield 4;
  }
}

この例はエラーとなります。gのなかのスコープではyieldを使うことができないからです。

finish!

「実はgeneratorIteratorを返す関数なのです!」

どうでしょうか? この言葉の意味がぼんやりながらも分かってもらえると幸いです。
最後に練習問題を用意しましたので、ぜひチャレンジしてみてください。

問題

1 take

Iteratorとnumberを引数に取り、指定された数以下の長さのIteratorを返す関数takeを実装してください。ここでいう長さとはnextを読んだときの返り値のdonetrueを返すまでの回数です。

ヒント1)take自体をgeneratorで書くと楽です。また、for~ofも便利です。

テストケース

Array.from(take(range(1000), 5)); // -> [0, 1, 2, 3, 4]

Array.from(take(range(3), 5)); // -> [0, 1, 2]

2 fib

フィボナッチ数列を返すようなIteratorを返す関数fibgeneratorで作ってみてください。

テストケース

Array.from(take(fib(), 20));

// -> [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]

3 split word

"Hello World Welcome To Generator"のような空白で区切られた文字列を引数に取り、word(空文字列を除く)を1つずつもらえるIteratorを返す関数splitWordを作ってみてください。

この問題ではyield*を使うと短く書くことができます。

テストケース

Array.from(splitWord("Hello  World Welcome To  Generator"))

// -> ["Hello", "World", "Welcome", "To", "Generator"]

脚注

脚注
1 take自体をgeneratorで書くと楽です。また、for~ofも便利です。