読者です 読者をやめる 読者になる 読者になる

#14 ECMAScript 6のやばい扉を開けちゃうかな(Generators編)

tech.kayac.com Advent Calendar 2014 14日目です!
ざっくばらんにES6のジェネレータのことを書きます。
実のところそんなにやばくないです。

ECMAScript 6!

ECMAScript 6、盛り上がってきましたね。
クラスアロー演算子let装飾子など、
魅力的な機能がいくつも追加されるES6ですが、
今回は、その中でもジェネレータについて書きます。

ジェネレータ!

ECMAScript 6で追加される新たな機能や構文、
"新たな"とはいえ、その多くが、シュガーシンタックス程度に留まっているのと比べ、
ジェネレータは、処理そのものの流れを大きく変えます。

ジェネレータはとりわけ、非同期処理を書くときに役に立ちます
Promisesasyncが、非同期処理を並べる程度の解決しかできないのに対して、
ジェネレータは、ネストを一切せずに、非同期処理を書くことができるようになります。

ジェネレータを利用した非同期処理は、今後主流になっていく気がするので、
2014年中に抑えておきましょう!

ジェネレータの基本

ジェネレータの基本的な使い方は、以下のとおりです。

// ジェネレータを返す関数を定義する
function *func(){
    yield 1;
    yield 2;
    yield 3;
    console.log(yield 5); // -> foo
}

// ジェネレータを取得する
var gen = func();

console.log(gen.next().value); // -> 1
console.log(gen.next().value); // -> 2
console.log(gen.next().value); // -> 3
console.log(gen.next().value); // -> 5
console.log(gen.next("foo").value); // -> undefined

出力は以下のようになります。

1
2
3
5
foo
undefined

ジェネレータの使い方

関数定義のとき、頭に*をつけることで、
ジェネレータを返す関数を定義できます。
そして、ジェネレータの中では、処理を中断するためのキーワードyield式が使えるようになります。

関数を呼び出すと、ジェネレータが返ります
ジェネレータは、nextというメソッドを持ち、
これを呼び出すことで、次のyield式まで処理を進め返り値 "value"などを含めたオブジェクトを返します

そして、次にnextを呼んだときは、前回のyield式から処理を再開します
また、nextに引数を与えることで、
再開するyield式の値とすることができます。

ジェネレータの応用

さて、実際のところ、ジェネレータは
RubyPythonなど、すでに多くのスクリプト言語で実装されていて、
プログラミングに精通している人ならば、
「あーあれね、やっとJSにもそういうのが追加されるのか。便利だね」
程度にしか考えないかもしれません。
しかし、コールバック地獄に苦しめ続けられてきたJavaScript界にとって
ジェネレータは、地獄に射す一筋の光に他なりません。

ジェネレータでコールバック地獄を解消する

一見して、ジェネレータはコールバック地獄と関係ないように見えますが、
応用することで、とても強力な道具になります。

重要なアイディアは、メインの処理をジェネレータにし、
それを外側からコントロールする構造を作ること
です。

非常に言葉で説明しづらいので、コードを貼ります。

function *main(){
    console.log("wait 1000ms");
    yield timeout(1000);
    console.log("calc 2*2");
    var four = yield getPower(2);
    console.log(four);
}

var timeout = function(time){
    return function(callback){
        setTimeout(callback, time);
    }
}

var getPower = function(value){
    return function(callback){
        setTimeout(callback, 3000, value*value);
    }
}

var gen = main();
var resume = function(arg){
    var thunk = gen.next(arg).value;
    if(thunk) thunk(resume);
}
resume();

上のコードを実行すると、 間隔を置きながら、次のような結果が出力されます。

wait 1000ms
calc 2*2
4

ネストが消えた!

ご覧の通り、timeoutなどの非同期処理を挟んでいるにもかかわらず、
メインの関数からはネストが消えました。
yield式が挟まっているので、最初はぎょっとするかもしれませんが、
慣れれば、非同期でネストしまくるコードよりも、はるかにわかりやすいはずです!

ライブラリを利用する

この仕組みを利用した代表的なライブラリにcoがあります。
coでは、上記と同じような書き方ができるのに加え、
エラーの取り回しや、Promisesとの連携などができるようになっています。

実際に取り入れるときは、こういったライブラリを活用しましょう。

ポリフィル

さて、紹介してきたジェネレータですが、
当然、古いIEなど、ジェネレータを実装していないブラウザでは利用できません。

それでもフロントで利用したい!場合には、
regeneratortraceur-compiler を使って、
ES5向けのコードに変換することもできます。

とはいえ、ジェネレータは最初に書いたように、かなり複雑な仕様。
少しばかりのソースの肥大化は許容しないといけないかもしれません。

終わりに

ジェネレータは、JavaScriptの非同期処理を大きく変える可能性のある機能です。
一方で結構癖があるのも事実なので、早めに触って慣れておくことをおすすめします!

明日は

次回はYAPC 2014で公演した@mackee_wさんです。 よろしくお願いします!