[JavaScript] 非同期処理のコールバック地獄から抜け出す方法

JavaScript (Node.js) で開発する上で避けては通れない
非同期処理、コールバックについて考えてみたい。
自分なりのお勧めの方式を書いてみた。
いろいろなものを試した結果である。

> ※この記事でのお勧めの方法は ES2015 (ES6) で実装された generators (yield)
　の技術を使用しています。
　実はまだ Babel(6to5) 等を利用するか Node.js v4～v8 でしか
　実質的に使用できない技術だと思います。悪しからず。
　(早く全てのブラウザに広く普及する事を祈っています)
　まだブラウザでは独自ライブラリか Promise (Deferred) 等を使っています。
※2015/10/15: 記事の内容を npm aa (async-await) に対応させました。
※2015/04/19: 記事の内容を npm co@4 に対応させました。
※2015/11/03: 追加記事を書きました。
[ES2017 (ES8) async/await + Promise で解決できる事、と ES2015 (ES6) generators (yield) + Promise + npm aa (async-await) で解決できる事 - Qiita]
(https://qiita.com/LightSpeedC/items/95e3db59276e5d1d1a0d)

# Agenda

* 非同期処理とコールバック
* コールバック地獄
* これからの非同期 aa
* aa.Channel を使った場合
* 今までの JavaScript
* 今でも超人気の async.js
* 必ず理解すべき Promise (Deferred)

# 非同期処理とコールバック

JavaScript はシングルスレッドで動くので、同時に複数の処理がしたい場合、
非同期処理を行うのだが、それには結果や完了をお知らせしてもらうための
コールバックが必要だ。

つまり非同期処理と言えば、コールバックだ。
JavaScript をやる以上、コールバックを避けて通る事はできない。
習得しなければいけない技術の1つだ。

コールバックにも同期型と非同期型がある。

コールバックには一度だけ必ず呼ばれるという前提のコールバックと、
呼ばれないかもしれないし、何度も呼ばれるかもしれない
イベント的なものがある。

それを踏まえて先に進もう。

# コールバック地獄

非同期な処理を実行するとコールバックが出てくる。
簡単な例を通して、どの様に実装できるのか考えてみる。

## 非同期な get という処理

get という関数を用意しておく。
サンプルは Node.js で書いてみた。

```js:get.js
'use strict';

function get(file, callback) {
  console.log('file: %s...', file);
  setTimeout(function () {
    console.log('file: %s complete', file);
    callback(null, '(' + file + ')');
  }, 200 + Math.random() * 100);
}

module.exports = get;
```

これはランダムに 200～300 msec くらい時間をおいて
答えを返す非同期処理の例である。
引数はファイル名やURLの文字列を想定しており、
返す値はその文字列を元にしている。

## get a, b, c をシーケンシャルに処理したい

a.txt, b.txt, c.txt の3つのファイルを順番に取得して、
結果を表示するサンプルを考えてみる。

それぞれ非同期処理なのでコールバックが呼ばれてから次の処理を開始している。

```js:callback-hell1.js
'use strict';

var get = require('./get');

get('a.txt', function (err, a) {
  get('b.txt', function (err, b) {
    get('c.txt', function (err, c) {
      console.log(a + b + c);
    });
  });
});
```

```bash
$ node callback-hell1.js
```

あぁ、まさにこれがコールバック地獄である。
3つの処理をシーケンシャルに実行するために、
関数の呼び出しがどんどん深くなってしまっている。
エラー処理なんて考えたくも無い。

ちょっと処理が複雑になったらどうするのだろうか。
ある条件の時、get b を呼び出さない、などというロジックを考えてみて欲しい。
あっちゅーまに複雑になる。

## get a, b, c をパラレルに処理したい

では、並行処理ができる様にしてみる。

```js:callback-hell2.js
'use strict';

var get = require('./get');

var n = 0;
var res = {};

++n;
get('a.txt', function (err, a) {
  res.a = a;
  if (--n === 0) callback(null, res);
});

++n;
get('b.txt', function (err, b) {
  res.b = b;
  if (--n === 0) callback(null, res);
});

++n;
get('c.txt', function (err, c) {
  res.c = c;
  if (--n === 0) callback(null, res);
});

function callback(err, res) {
  console.log(res.a + res.b + res.c);
}
```

```bash
$ node callback-hell2.js
```

生の JavaScript だとこれくらいのロジックで勘弁して欲しい。
この方式だと、ある条件の時 get b を呼ばない、とか簡単だ。

## get a, b を並列に、その後 get c を

では、get a と get b を並列にやって最後に get c をやってみる。

```js:callback-hell3.js
'use strict';

var get = require('./get');

var n = 0;
var res = {};

++n;
get('a.txt', function (err, a) {
  res.a = a;
  if (--n === 0) callback(null, res);
});

++n;
get('b.txt', function (err, b) {
  res.b = b;
  if (--n === 0) callback(null, res);
});

function callback(err, res) {
  get('c.txt', function (err, c) {
    res.c = c;
    console.log(res.a + res.b + res.c);
  });
}
```

```bash
$ node callback-hell3.js
```

うむ。複雑だが、がんばればできるね。
でも、やってる事に比べてソースの行数が多くないか?
コピペが多いし。
これでいいわけはない。

# これからの非同期 aa

いろんなライブラリを試してやっとたどり着いたのが aa だ。

申し訳ないが ES2015 (ES6) generators (yield) が有効になっていないといけない。

最近のブラウザでは Firefox, Chrome, Edge は常に有効の様だ。
IE はあきらめろ。babel 使ってください。

## aa の準備

aa の準備をしておこう。

```bash:npm-install-aa.sh
$ npm i aa -S
```

または

```bash:npm-install-aa-save.sh
$ npm install aa --save
```

aa 経由で get を使うために非同期関数 get を
`aa.thunkify` または `aa.promisify` で wrap する。
Promise スタイルにしたい時は、promisify を使うと良い。
※ややこしい fs.exists や child_process.exec にも対応している。

## aa: get a, b, c をシーケンシャルに処理する

aa を使ってシーケンシャル処理を書いてみる。

```js:aa-get1.js
'use strict';

var aa = require('aa');
var get = aa.thunkify(require('./get'));

aa(function*() {
  var a = yield get('a.txt');
  var b = yield get('b.txt');
  var c = yield get('c.txt');
  console.log(a + b + c);
}).then(
  function (val) { console.info('ok: ' + val); },
  function (err) { console.error('ng: ' + err); });
```

実行するには以下のコマンドを使う。

```bash
$ node aa-get1.js
```

おぉ、メインロジックはすっきりだね。
シーケンシャルそのままだ。

実はエラー処理も try catch で良い。これはいい。

## aa: get a, b, c をパラレルに処理する

aa を使ってパラレル処理を書いてみる。

```js:aa-get2.js
'use strict';

var aa = require('aa');
var get = aa.thunkify(require('./get'));

aa(function*() {
  var res = yield {a: get('a.txt'),
                   b: get('b.txt'),
                   c: get('c.txt')};
  console.log(res.a + res.b + res.c);
}).then(
  function (val) { console.info('ok: ' + val); },
  function (err) { console.error('ng: ' + err); });
```

```bash
$ node aa-get2.js
```

おぉ、やっぱりすっきり。
パラレル感がはんぱねぇ。

配列もオブジェクトも yield できるので
並列に処理する数が不明の場合も対応できるね。
エラー処理も try catch できるし。

## aa: get a, b を並列に、その後 get c を

aa を使って get a, b を並列に、その後 get c を。

```js:aa-get3.js
'use strict';

var aa = require('aa');
var get = aa.thunkify(require('./get'));

aa(function*() {
  var res = yield {a: get('a.txt'),
                   b: get('b.txt')};
  res.c = yield get('c.txt');
  console.log(res.a + res.b + res.c);
}).then(
  function (val) { console.info('ok: ' + val); },
  function (err) { console.error('ng: ' + err); });
```

```bash
$ node aa-get3.js
```

おぉ、これも、なんか、いい感じじゃない?
これなら、いろいろなロジックが書けそうな気がする。
aa で JavaScript の世界が変わると思う。
aa 最高!

# aa.Channel を使った場合

もうこれで解決した気になったかもしれないが、
非同期処理やコールバックには、実は他のパターンもある。

イベントやストリームだ。
aa と aa.thunkify/aa.promisify だけでは解決できない。
aa.Channel というのもあるので、別パターンという事で実験的に書いてみる。

## aa.Channel を使ったシーケンシャル処理

aa.Channel を使ってシーケンシャル処理を書いてみる。

コールバックとしてチャネルを渡し、結果を得るためには yield すれば良い。
チャネルはキューやバッファの様なものだと思えばいい。

```js:aa-chan1.js
'use strict';

var aa = require('aa');
var Channel = aa.Channel;
var get = require('./get');

aa(function*() {
  var ch = Channel();

  get('a.txt', ch);
  var a = yield ch;

  get('b.txt', ch);
  var b = yield ch;

  get('c.txt', ch);
  var c = yield ch;

  console.log(a + b + c);
}).then(
  function (val) { console.info('ok: ' + val); },
  function (err) { console.error('ng: ' + err); });
```

```bash
$ node aa-chan1.js
```

あまり aa + Promise や aa + thunkify パターンと変わらないが get 関数は wrap されていない。
Node スタイルの非同期関数のままだ。

## aa.Channel を使った並列処理

aa.Channel を使って並列処理を書いてみる。

```js:aa-chan2.js
'use strict';

var aa = require('aa');
var Channel = aa.Channel;
var get = require('./get');

aa(function*() {
  var ch_a = Channel();
  var ch_b = Channel();
  var ch_c = Channel();

  get('a.txt', ch_a);
  get('b.txt', ch_b);
  get('c.txt', ch_c);
  var a = yield ch_a;
  var b = yield ch_b;
  var c = yield ch_c;

  console.log(a + b + c);
}).then(
  function (val) { console.info('ok: ' + val); },
  function (err) { console.error('ng: ' + err); });
```

```bash
$ node aa-chan2.js
```

get を続けて呼べば、並列処理だし、yield すればそこで同期できるのだ。
get が完了する順番が変わっても、この場合は正しく同期できる。

## aa.Channel: get a, b を並列に、最後に c を

aa.Channel で a, b を並列に、最後に c の処理を。

```js:aa-chan3.js
'use strict';

var aa = require('aa');
var Channel = aa.Channel;
var get = require('./get');

aa(function*() {
  var ch_a = Channel();
  var ch_b = Channel();
  var ch_c = Channel();

  get('a.txt', ch_a);
  get('b.txt', ch_b);
  var a = yield ch_a;
  var b = yield ch_b;

  get('c.txt', ch_c);
  var c = yield ch_c;

  console.log(a + b + c);
}).then(
  function (val) { console.info('ok: ' + val); },
  function (err) { console.error('ng: ' + err); });
```

```bash
$ node aa-chan3.js
```

並列も直列も思いのままだ。
コールバックとしてチャネルを渡し、同期したい時に yield するだけだ。

aa.Channel は get などを Promise や thunkify 等で wrap しなくて良いという利点があるが、
見映えはあまり良くないので、こういうパターンでは aa + Promise または aa + thunkify をお勧めする。
記述量が少なく簡単な方がいいからね。

## aa.Channel でスレッド間通信の様な事

aa.Channel ではスレッド間通信の様な事ができる。

2つのスレッド間で、データをやりとりする例を書いてみる。

```js:aa-chan4.js
'use strict';

var aa = require('aa');
var Channel = aa.Channel;

var ch1 = Channel();
var ch2 = Channel();

// *** FIRST THREAD ***
aa(function*() {
  // send
  ch2('(a)');

  // receive
  var b = yield ch1;
  console.log(b);

  // send
  ch2('(c)');

  console.log('end of thread 1');
}).then(
  function (val) { console.info('ok: ' + val); },
  function (err) { console.error('ng: ' + err); });

// *** SECOND THREAD ***
aa(function*() {
  // receive
  var a = yield ch2;

  // send
  ch1('(b)');

  // receive
  var c = yield ch2;

  console.log(a + c);
  console.log('end of thread 2');
}).then(
  function (val) { console.info('ok: ' + val); },
  function (err) { console.error('ng: ' + err); });
```

```bash
$ node aa-chan4.js
```

ちなみにスレッドの事を説明せねばなるまい。
最初に書いたが JavaScript はマルチスレッドではない。
シングルスレッドなのだ。
ここでスレッドと書いたが本当はスレッドもどきだ。
Non-preemptive Multitasking かな。
Windows 3.1 (16bit) のメッセージ処理と同じだ。
自分がここで処理を開放しても良いと思った時にだけ
他の処理に制御が移せる。
co-routine だ。

## イベント処理やストリーム処理

aa.Channel が得意なパターンとして、イベント処理やストリーム処理がある。
aa と thunkify/promisify だけでは、コールバックが1回だけ呼び出されるという前提があり、
その上で yield が可能なのであったが、
aa.Channel ではコールバックが複数回呼び出されることが前提となっている。

今は面倒なので詳しく書かないが socket などのストリームからのイベント処理を以下の様に記述できる。

```js
var ch = Channel();
ch.stream(socket);
// socket.on('end', ch.end);
// socket.on('error', ch);
// socket.on('readable', ch.readable);

var buff;
while (buff = yield ch) {
  console.log(String(buff));
}
```

# 今までの JavaScript

もう、今までの JavaScript では書けない。
コールバック地獄から早く抜け出そう。

# 今でも超人気の async.js

async.js は恐らく最初に発見する非同期処理のライブラリだ。
いっぱい使われている様でダウンロードされまくっている。
名前が良いんだね。
map とか filter とか、いろいろあって非常に便利だと思う。
でも function キーワードがコード中にたくさん出てくるので、
私はもう使っていない。

# 必ず理解すべき Promise (Deferred)

ES2015 (ES6) Promise (Deferred) は絶対おぼえておかなきゃならない。

Node.js ではサーバなので、開発側というかサービスを提供する側が、
バージョンや起動オプションを決めることができるので、
yield ベースの aa や co なんかを使う事ができる。

だが、ブラウザ(クライアント)側では、そうはいかない。
yield が使える様になるまでは、npm bluebird や jQuery などのライブラリが提供している
Promise (Deferred) を勉強しておこう。

ちなみに aa や co でも Promise (thenable) が yieldable なので、いいよね。
意味不明。

# co ベースの koa(koajs) に進もう

express の開発者達が co ベースの koa を作っている。
これはもう使わない手は無い。

サーバ側なのだから ES2015 (ES6) generators/yield はもう使える技術なのだ。

※記事書いてます → [Node.js + co + koa の勧め - Qiita](https://qiita.com/LightSpeedC/items/bcc92391b5436664d4cb)

# aa や co の暗黒面

夢の様な力を持つ aa や co だが、実はダークサイドもあるのだ。

非同期処理のコールバックは非常にうまく使えるのだが、
同期的なコールバックと yield を組み合わせると、崩壊するのだ。ははは。

```js
'use strict';

var aa = require('aa');

aa(function*() {
  var ary = [1, 2, 3];

  ary.forEach(function (val) {
    console.log(val);
  });
}).then(
  function (val) { console.info('ok: ' + val); },
  function (err) { console.error('ng: ' + err); });
```

配列の forEach の引数の関数の中には yield が書けないのだ。
こういう場合は for 文で ary[i] を回せ。ダサいけど。

同期的なコールバックの例としては Array の forEach, map, filter や
String の replace など、探せばたくさんある。

map と arrow function を組み合わせれば、うまく対応できるよ。
以下のコードを見て欲しい。
map の中は非同期な処理が複数含まれていれば並行処理となる。

```js
'use strict';

var aa = require('aa');

aa(function*() {
  var ary = [1, 2, 3];

  return yield ary.map(val => function* () {
    console.log(val);
    yield Promise.resolve(val);
  });
}).then(
  function (val) { console.info('ok: ' + val); },
  function (err) { console.error('ng: ' + err); });
```

# 推奨URL / 参考文献

以下のリンクを是非読んで欲しい。

[コールバック……駆逐してやる…この世から…一匹…残らず!!]
(https://qiita.com/KDKTN/items/4c6986049d204f0645d8)

[JavaScriptの非同期処理には何を使うべきか]
(https://qiita.com/zoetro/items/bee9567349577620860a)