memolu

いろいろメモってます

ES6 - Generator を解説してみる

本連載について

本連載では少しづつES6について記載していこうと思います。 第四回はGeneratorを学びます。 発表されてから久しいですが、まだ習得されていない方は今からでも勉強してみてはいかがでしょうか。

Generator

Generatorとは「関数の途中で抜けて、また処理を再開して」を実現する機能です。一時停止と再生のようなものを想像してもらうとわかりやすいかもしれません。
処理を途中で抜ける回数はコードの書き方次第で何回でも可能で、そのたびに任意の値を取り出すこともできます。

ブラウザサポート

ブラウザサポートについては以下をご確認ください。

http://kangax.github.io/compat-table/es6/

Generator関数の使い方

通常の関数宣言文ではfunction() {}のように宣言されますが、Generator関数ではfunction* () {}のように*が追加されます。
まずはテストコードを記載しましょう。これが一番簡単なGeneratorの形になります。

function* genetatorFunc {
    yield 1;
    yield 2;
    return 3;
}

yieldという見たことない記述も現れました。こちらも合わせて以下で解説していきます。

Generator関数を呼び出すと、通常の関数とは違ってGeneratorオブジェクト(イテレータオブジェクト)というものを返します。
Generatorオブジェクトはnext()メソッドというものを持っていて、Generator関数内の一時停止と再生はこれを通して行えるようになります。 上記のコードに少し記述を追加してみましょう。

function* genetatorFunc {
    yield 1;
    yield 2;
    return 3;
}

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

変数generatorを宣言し、宣言したgeneratorFunc()関数を代入しました。これにより変数generatorにはGeneratorオブジェクトが格納されました。
さらに、console.log()の中でnext()を実行しているところに注目してください。
next()については次項で解説します。

next()

next()が実行されると、Generator関数の中にあるyieldの式まで評価し、評価結果を返します。つまり1度目のnext()ではyield 1;までが実行されたということです。
2回目のnext()ではyield 2;が実行されています。このようにnext()がyieldと紐付くことで一時停止と再生を実装しています。

next()が実行された場合の返り値にも注目してみましょう。返り値はvalueとdoneというプロパティを持ったオブジェクトになります。
上のサンプルコード内にある以下の部分をご覧ください。

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

valueにはyieldの評価式に記載した値が返ってきています。
doneというプロパティにはboolean型で値が入ります。
これらについて以下で説明いたします。

value

valueはyieldに記載した評価式の評価結果が出力される場所です。
サンプルコードでは以下になります。

function* genetatorFunc {
    yield 1;  // ここの評価結果がvalueの値として返る
    ~ 略 ~
}

yieldに記載した1がそのまま返ってきているというわけです。
また、評価式は「式」なので例えばyield 1 + 10;のようにすれば11が返るようになります。

done

doneはGeneratorオブジェクトから全ての値を取り出したかをboolean型で示します。
この値がfalseであればGeneratorオブジェクトの中にはまだ値が残っているということになります。そして、Generator関数の中でreturnすると値は全て返し終わったと処理され、doneはtrueになります。

function* genetatorFunc {
    yield 1; // 1度目ではここまで & done: false
    yield 2; // 2度目ではここまで &  done: false
    return 3;  // 3度目ではここまで &  done: true
}

それではreturnが記載されていない場合はどうなるのでしょうか。
以下をご覧ください。

// returnが記載されていない場合
function* generatorFunc() {
    yield 1;
    yield 2;
    yield 3;
}

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

returnを記載せず全てyieldのみで記載した場合全ての値を取り出し終わったにも関わらずdoneはfalseになります。
returnで取り出し終わったことを名言する必要がありますのでyield 3;のタイミングではまだ取り出し終わったことになっていないのです。
4回目のnext()が実行された際にvalue: undefinedとなり、そこでdone: trueになりました。

続いて、returnの後でyieldを書いた場合はどうなるのでしょうか。
こちらも記載しておきます。

//returnの後でyieldを書いた場合
function* generatorFunc() {
    yield 1;
    yield 2;
    return 3:
    yield 4;
}

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

このケースの場合、他のjavascriptの処理と同様、return以降は値を書いていたとしても読み込まれることはありません。そのため4回目のnext()ではyieldは記載されているにも関わらずvalueの値にはundefinedが返ってきています。

上記のケースはあまりおすすめできるコードではありませんが、tipsとして仕様を把握するためにご紹介させていただきました。

yield*

Generatorオブジェクトの中でyield*を使用すると、値をそのまま返すのではなくiterableとして扱うことが可能です。

iterable
イテレータとは、一連の処理中において現在の処理位置を把握しつつ、コレクション中のアイテムへ一つずつアクセスする方法を備えたオブジェクトのことです。JavaScript においては、イテレータは一連の処理中の次のアイテムを返す next() メソッドを提供するオブジェクトです。このメソッドは done と value という 2 つのプロパティを持つオブジェクトを返します。
イテレータとジェネレータ - JavaScript | MDN

イテレータはつまり反復可能なオブジェクトとして捉えていただければ問題ありません。 コードを見たほうがわかりやすいため、まずは下のコードをご覧ください。

function* count(n) {
    yield n;
    yield* count( n + 1 );
}

var generator = count(10);
console.log(generator.next()); // => { value: 10, done: false };
console.log(generator.next()); // => { value:11, done: false };
console.log(generator.next()); // => { value: 12, done: false };

yield*を使用すると何度も同じ処理を続けることが可能になります。このサンプルコードの場合、作成したcount()自体もイテラブル(反復可能)なため、next()で何度も呼び出しつつカウントアップすることができました。

エラーハンドリング

ここではGenerator関数の中でエラーが発生した場合の処理方法を解説します。 Generator関数内で発生したエラーは、`next()=の呼び出し元で受け取ることができます。
下のコードをご覧ください。

// yieldの評価結果をthrow文で返す
function* generatorFunc() {
    throw new Error('エラーメッセージ');
}

var generator = generatorFunc();
try {
    generator.next();
} catch(e) {
    console.error(e); // Error: エラーメッセージ
}

このようにGenerator関数の中で例外(throw文)が発生した値をtry~catch文で受け取ることが可能です。
エラーハンドリングが必要な場合、基本はこの書き方をすればよいでしょう。

for~of文との組み合わせ

同じくES6で実装された構文にfor~of文があります。
for~of文はプロパティや配列の値を取得してくるものです。for~in文ですとオブジェクトのプロパティ名を取得してきたものに対し、こちらは値を対象としているわけです。
また、for~of文はiterableなオブジェクトに対し実行することを前提として仕様策定されているため、Generatorオブジェクトに対しても問題なく動作します。

function* generatorFunc() {
    yield 1;
    yield 2;
    return 3;
}

var generator = generatorFunc();
for(var n of generator) {
    console.log(n);
}

これを実行するとconsoleには以下のように出力されます。

1
2

return文で返り値にした3が出力されていませんね。これはfor~of文がdoneプロパティの値がtrueの場合、値を取得しないように設計されているからです。使用する際はこの点に注意してください。

Generator関数へ値を渡す

今まではnext()を使用してgeneratorオブジェクトから値を受け取る方法をご紹介しました。それでは逆にgeneratorオブジェクト側で値を渡したい場合はどうしたらよいのでしょうか。
この場合は、next()の引数として値を渡すことで実装できるようになっています。渡された値は、generator関数の中で変数を設定することで任意の変数に代入することができます。渡したい変数には以下のようにyieldを代入しておいてください。

var a = yield;

続いて、下のコードをご覧ください。

function* generatorFunc() {
    var first = yield 1;
    console.log(first);
    var second = yield 2;
    console.log(second);
}

var generator = generatorFunc();
generator.next();
// { value : 1 , done: false }
generator.next('first message');
// { value: 2, done: false } & consoleに「first message」
generator.next('second message');
// { value: undefined, done: true } & consoleに「second message」

上のコードを実行するとそれぞれコメントアウトで記載したように出力されます。 少し処理の順序が複雑になりますので順を追って説明してみます。

1回目のnext()

1回目のnext()では処理は下のコードの位置までしか進んでいません。 処理的には最初のyieldの式が評価され、値として返ってきただけになります。 1回目のnext()ではまだyieldの式を評価するだけになりますので、ここで引数として何かを渡しても処理されません。
そのため引数は2回目から入力しています。

function* generatorFunc() {
    var first = yield 1;
}

var generator = generatorFunc();
generator.next();
// { value : 1 , done: false }

2回目のnext()

2回目のnext()では以下の位置まで処理が進行します。
まず引数として渡された'first message'var firstに代入されます。そしてconsoleに出力されyield 2まで進み、式を評価して値として返しています。

function* generatorFunc() {
    var first = yield 1;
    console.log(first);
    var second = yield 2;
}

var generator = generatorFunc();
generator.next();
// { value : 1 , done: false }
generator.next('first message');
// { value: 2, done: false } & consoleに「first message」

3回目のnext()

そして3回目でここまで処理が進みます。
引数として渡された'second message'var secondに代入され、consoleに出力されています。次に処理すべきyieldがないため返り値のvalueプロパティにはundefinedが返ります。

function* generatorFunc() {
    var first = yield 1;
    console.log(first);
    var second = yield 2;
    console.log(second);
}

var generator = generatorFunc();
generator.next();
// { value : 1 , done: false }
generator.next('first message');
// { value: 2, done: false } & consoleに「first message」
generator.next('second message');
// { value: undefined, done: true } & consoleに「second message」

値を渡す際には、このようにgenerator関数の中で処理の順序を意識するとしっかりコードの設計ができるようになります。

Promiseとの連携

generator関数は任意のタイミングで一時停止・再開を行うことができるものでした。そしてここまででGenerator関数の説明は完了しています。
本項ではこの機能を使って非同期処理を同期的に扱う方法をご紹介します。ES6の非同期処理といえばPromiseになりますが、これとGenratorを組み合わせてコードを書いてみようと思います。

ここで解説を進行したいところなのですが、ここで先に少し補足をさせてください。

ES7のasync/awaitについて

今回解説しているのはES6になりますが、ES7についても仕様策定が着々と進んでいます。その中でもasync/awaitは今回解説する機能をよりシンプルにできる機能で、これがトランスパイラであるbabelが対応したことにより、どれがベストプラクティスかはコードによって検討する余地が残っています。

qiita.com

Generatorもあくまで処理の一時停止と再生を実現する機能であり、Promiseとの連携はあくまでもコードのデザインパターンとして存在しているだけです。
何が言いたいかというと、上述の通り本項で解説するコードをベストプラクティスと呼ぶには疑問の余地が残るということです。あくまでもそれらを考慮した上で、読み進めていただければと思います。

Promiseを同期処理する

改めてここからGeneratorとPromiseの連携を説明します。
APIのような機能を想定してコードを記載しますが、あくまでサンプルのため実際のサーバー通信処理部分は割愛してコードを書いていきますのでご了承ください。

今回は、あるショッピングサイトでカートの中身を表示させる機能を想定します。 処理はシンプルに以下のような流れを想定しています。

  • getUserIDでIDを取得
  • IDをもとにgetUserCartでカートの中身をcartListとしてjsonで取得
  • cartListをreturn

最終的に取得できるcartListは以下のようなフォーマットと仮定します。

var cartList = fetchUserCart(ID);
console.log(cartList);
// [
//     {
//         "name": "あいてむ1",
//         "price": "10,000"
//     },
//     {
//        "name": "あいてむ2",
//        "price": "100"
//     }
// ]

前置きが長くなりましたがそれではコードを書いていきます。
まず大枠としてGenerator関数を作成しましょう。

function* fetchUserCartList(name) {
    var id = yield getUserID(name);
    var cartList = yield getUserCartList(id);
    return cartList;
}

function getUserID(name) {
    return new Promise(function (resolve, reject) {
        // ~ 略 ~
        resolve(res);
    });
}

function getUserCartList(ID) {
    return new Promise(function (resolve, reject) {
        // ~ 略 ~
        resolve(res);
    });
}

こんな感じになります。 yieldにそれぞれ記載されているgetUserID();getUserCartList()はともにPromiseで処理が記載されているものと仮定してください。 Promiseだけで非同期処理を長く書くよりかなりシンプルにできています。
次にGeneratorオブジェクトを取得する方法です。

var generator = fetchUserCartList('yt_ymmt');
var generatorResult = generator.next(); // { value: Promise, done: false }
var promiseID = generatorResult.value;

このようにyieldの評価式にPromiseオブジェクトを返すようにすると、valueの値としてPromiseオブジェクトをそのまま受け取ることが可能です。
promiseIDには関数getUserID()のpromiseオブジェクトが格納されています。非同期通信で取得したIDを受け取るために、このpromsieがFullfilledになった場合の処理を記載します。

promiseID.then(function(id) {
    var generatorResult = generator.next(id);
});

Promise.then()を使って、promiseIDがFullfilledになったら(IDが取得できたら)コールバック関数内でGenerator関数をnext()します。コールバック関数の引数にはresolve()からIDが渡ってきています。Generator関数にIDを渡す必要があるため、next()をする際にIDも忘れずに引数として渡します。こうすることでGenerator関数内のvar IDにIDを渡しつつgetUserCartList(id)を実行することができます。

getUserCartList(id)でもPromiseが返ってきますので受け取る変数promiseCartを用意しておきます。

promiseID.then(function(id) {
    var generatorResult = generator.next(id);
    var promiseCart = generatorResult.value;
    return promiseCart;
});

そしてpromiseCartに格納されているPromiseオブジェクトがFullfilledになった場合の処理を追加します。ここまでくれば後は簡単で、単純にcartListの値を受け取るだけです。

promiseID.then(function(id) {
    var generatorResult = generator.next(id);
    var promiseCart = generatorResult.value;
    return promiseCart;
}).then(function(cartList){
    var generatorResult = generator.next(cartList);
    console.log(generatorResult);
    // generatorオブジェクトのvalueの値にカートの中身が出力されたjsonが表示される
});

求めていた情報を取得することができました。 このようにnext()を処理の進行係にすることで非同期処理を同期的に処理することができるようになります。

まとめ

generatorは単体でも機能しますが、最後にご紹介させていただいたように他のプログラム処理と組み合わせて使うことでさらに利便性の向上が期待できます。ぜひ使いこなしてより見通しのいいコードを書けるようになりたいですね。