Node.jsでの非同期処理を、coとthunkifyを使って幸せに書こう

みなさんNode.jsは好きですか？
僕は大好きです。

Javascriptは別に嫌いじゃないけど、非同期処理がねえ...という方もたくさんいるかと思います。
そこで今回はいかにして非同期処理を、そしてコールバック地獄を倒すかという話です。

generatorを使うので古いNode.jsでは動かない気がします。

# とりあえず動かしてみる

適当なディレクトリを作って入り、coとthunkifyをインストールして、
`index.js`を作って、実行してみます。

```bash:shell
$ yarn add co thunkify
# もしくは npm install co thunkify
```

```js:index.js
const co = require('co')
const thunkify = require('thunkify')
const fs = require('fs')

fs._readFile = thunkify(fs.readFile) // 名前が適当なのは許してください

co(function*() {
  var file = yield fs._readFile('index.js')
  console.log(file.toString())
}).catch(console.error)
```

さて、これを実行してみましょう。

```bash:shell
$ node index.js
# const co = require('co')
# ...
# 以下index.jsの内容が表示される
```

# coとthunkifyとかyieldとは何じゃ

## yieldとgenerator
まず７行目にある`function*`という記述ですが、これはgeneratorというやつです。
この宣言により作られるのは、functionではなくて、generatorというもので、これは他の言語のgeneratorとかと同じように、複数個の値を、呼び出すたびに１つづつ返すものです。
例えば、1から順に2,4,8,16,32,64...と返すような関数や、
とても長いファイルを呼び出すたびに1行ずつ読み込んで内容を返すような関数なんかが、
典型的にgeneratorといわれて想像するようなものですね。

yieldはreturnと似ているのですが、そのgeneratorを完全に終了するのではなく、一度値を返してgeneratorはその状態で待機するというようなイメージです。

つまり今回の`index.js`では、`yield fs._readFile('index.js')`というものの返り値を返却して、このgeneratorは一度停止します。

## thunkify

さて、`fs._readFile`というのは、何ぞやということで見てみると`thunkify(fs.readFile)`と代入しています。
この[thunkify](https://github.com/tj/node-thunkify)ですが、ざっくりと働きを説明すると、callbackを引数に取るfunctionを、Promiseに変換してくれるものです。

```js:thunkifyのイメージ
function hoge(a, callback){
  setTimeout(callback, 1000)
}
var fn = thunkify(hoge)
fn(1) // これはPromiseが返ってくる
```

このモジュールは1ファイルしかなくて、とても簡単なので、直接読んでみるといいと思います。
[tj/node-thunkify](https://github.com/tj/node-thunkify/blob/master/index.js)

大体のNode.jsのfunctionはcallbackを利用しているので、これでPromiseに対応させます。

## co

最後に[co](https://github.com/tj/co)ですが、これはgeneratorがPromiseを返り値として返してきたら、Promiseがresolveするまで待って、その返り値を受け取って、generatorを再実行してくれます。

つまり`index.js`では、readFileにより読み取られたファイルが、`yield fs._readFile('index.js')`の評価値となって、再度generatorが再開されます。
これによって、あたかもfs.readFileを同期処理的に書けたことになります。
(readFileSyncと何が違うんじゃいというのは、この後書きます)

ちなみに、fs.readFileのcallbackの引数は(err, data)となっていますが、
第1引数のerrがnullでなかった場合、Promiseがrejectされます。
そしてyieldで帰ってくるのは第2引数以降です。

何とcoも1ファイルです！！
[tj/co](https://github.com/tj/co/blob/master/index.js)
しかも書いているのは同じ方ですね!!!


# coとthunkifyのありがたみ

## 並列実行できるぜ！

さっきの例では、fs.readFileSyncしているのと同じでした。
じゃあ、何がいいのかというと、とても簡単に並列に実行して、結果を配列で受け取れます。

```bash:shell
$ yarn add request
# もしくは npm install co request
```

```js:parallel.js
const co = require('co')
const thunkify = require('thunkify')

const request = thunkify(require('request'))

co(function*() {
  var response = yield [
    request('http://www.google.com'),
    request('http://www.yahoo.co.jp'),
    request('http://qiita.com')
  ]
  console.log(response)
}).catch(console.error)
```

何と、これだけで、並列にGETリクエストを送ることができます。

GETリクエストと同時に、ファイル読み込みもできるし、
結果をHashとして受け取ることもできる。

```js:index.js
const co = require('co')
const thunkify = require('thunkify')
const fs = require('fs');

const request = thunkify(require('request'))
fs._readFile = thunkify(fs.readFile)

co(function*() {
  var response = yield {
    qiita: request('http://qiita.com'),
    file: fs._readFile('index.js')
  }
  console.log(response)
}).catch(console.error)
```

とても便利ですね！！

# co.wrap

補足なのですが、coで囲むとそのまま即時実行されてしまいます。
普通のfunctionが欲しいときはどうすればいいかというと、

```js:co-wrap.js
const co = require('co')
const thunkify = require('thunkify')

const request = thunkify(require('request'))

var parallelRequest = co.wrap(function*(urls) {
  var promises = urls.map((url) => request(url))
  return yield promises
})

var urls = ['http://qiita.com', 'https://google.co.jp']
parallelRequest(urls).then((arr) => {
  console.log(arr)
}).catch(console.error)

```

これによって、generatorがPromiseを返すfunctionになってくれます。

むむ、この感じ、さっき聞き覚えがありますね。
thunkifyは、callbackをとるfunctionをPromiseを返すfunctionにする。
co.wrapは、generatorをPromiseを返すfunctionにする。
つまり、co.wrapで得られたfunctionもまたyieldできます。
便利ですね！

## 必ずcatchしよう
Promiseの例外ですが、catchしないとNodeのバージョンによっては握りつぶされることがあります。
割と簡単にデバッグを難しくさせるので、少なくとも`console.log`ぐらいは書いておくようにしましょう。

以上です。

ここ間違ってるよとか、ここ違和感があるよとか、
あと日本語が適当すぎるよなどのツッコミもお待ちしております。