coとthunkifyとgenerator

JavaScriptのgeneratorを調べると一緒に出て来やすい、coを使ってみたメモ。

generator based flow-controlとあるけどgeneratorやPromiseをいい感じに扱ってくれるライブラリ、という理解で良いのかな。いい感じに、というところがよくわかってない上にここの理解が一番大事だと思うのだけど。


あらかじめ

$ npm install co

をしておく。

それと、generatorを使うので実行する際はnode.js ver.0.11を使い、オプションに--harmonyもしくは--harmony-generatorを指定する。

var co = require('co');

function wait() {
  return new Promise(function(resolve, reject) {
    setTimeout(resolve, 1000);
  });
}

co(function* () {
  console.log('Hello');
  yield wait();
  console.log('World!');
}).catch(function(err) {
  throw err;
});
$ node --harmony index.js
Hello
World!

Promiseを返す関数をcoに渡したgenerator内でyieldを使って同期的に書いている。エラーのハンドリングはcoが返すPromiseのcatchで行っている。

このPromiseはbluebirdなどのものでなくて純正のES6のPromiseなのでcatchを(もしくはthenの第二引数に渡す関数)を書いておかないと構文エラーでさえも出力されないので、どんなときでも書いておいた方が良いと思う。(とか書いておいてこのページでも一部書いてない)

yieldとgeneratorはそんなにいいかな、と思ってたのだけど同期的に書けるとこれはこれで良いと思った。今までのコールバック地獄みたいなものでも浅ければ慣れてるので良いのだけど。

var co = require('co');

function throwError() {
  return Promise.reject(new Error('error!'));
}

co(function* () {
  try {
    yield throwError();
  } catch (e) {
    console.error('caught error!');
    console.error(e.stack);
  }
});
$ node --harmony index.js
caught error!
Error: error!
    at throwError (/path/to/index.js:4:25)
    at /path/to/index.js:9:11
    at GeneratorFunctionPrototype.next (native)
    at onFulfilled (/path/to/node_modules/co/index.js:55:17)
    at co (/path/to/node_modules/co/index.js:44:10)
    at Object.<anonymous> (/path/to/index.js:7:1)
    at Module._compile (module.js:460:26)
    at Object.Module._extensions..js (module.js:478:10)
    at Module.load (module.js:355:32)
    at Function.Module._load (module.js:310:12)

try-catchでエラーもつかめる。

var co = require('co');

co(function* () {
  yield function* () {
    setTimeout(function() {
      console.log(5);
    }, 0);
  };
});
$ node --harmony index.js
5

anonoymous-generator?も実行できる。

var co = require('co');

function get() {
  return Promise.resolve(5);
}

co(function* () {
  var num = yield get();
  console.log(num);
}).catch(function(err) {
  throw err;
});
$ node --harmony index.js
5

値を受け取って出力する。yieldはつけないといけないけど、普通の関数を実行して値を受け取るようにして書く。


coと同じくよく使われるらしいthunkifyもついでに使ってみた。

コールバック引数を渡す関数をcoで使い易いように変換するモジュール、でいいのかな。

あらかじめ

$ npm install thunkify

をしておく。

var co = require('co'),
    thunkify = require('thunkify');

function get(cb) {
  cb(null, 'str');
}

co(function* () {
  var get_ = thunkify(get);
  var str = yield get_();

  console.log(str);
});
$ node --harmony index.js
str

コールバック関数を受け取るget()をthunkifyで変換して、それをco内でyieldして同期的に値を受け取ってる。面白い。

var co = require('co'),
    thunkify = require('thunkify');

function get(num) {
  return Promise.resolve(num);
}

co(function* () {
  var results = yield [
    get(1),
    get(2),
    get(3)
  ];
  console.log(results);
});
$ node --harmony index.js
[ 1, 2, 3 ]

Promiseの並列実行もできる。受け取った配列には関数を書いた順番に戻り値が入る。

var co = require('co'),
    thunkify = require('thunkify');

function get(num, timeout) {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      console.log(num);
      resolve(num);
    }, timeout);
  });
}

co(function* () {
  var results = yield {
    a: get(1, 3000),
    b: get(2, 2000),
    c: get(3, 1000)
  };
  console.log(results);
});
$ node --harmony index.js
3
2
1
{ a: 1, b: 2, c: 3 }

オブジェクトで受け取るとそれぞれのキーにそれぞれの戻り値が入る。


慣れないとわからない上にPromiseの知識も前提として必要になってくると思うけど、慣れればだいぶ読みやすいと思う。

コールバック関数を渡したり、Promiseで書いたりということが多かったのだけど、それらになれたせいでこれらの同期的な書き方に違和感がある。でも読みやすくて良い。