Promiseを返す関数を同期的に繰り返し実行したい

最近はNode.jsで簡単なスクリプトを実行することが多く、その時に改めてasync/awaitわかりやすいなーと感じることがあったのでそれについてつらつら記述していきます。

- 手っ取り早く結果を知りたい人は以下の部分だけ読むと良いと思います。
  - [何をしたいか](#何をしたいか)
  - [何が問題なのか](#何が問題なのか)
  - [async/awaitがあるとあっさり解決](#asyncawaitがあるとあっさり解決)
  - [確認のために実行できる例](#確認のために実行できる例)
  - [async/awaitがない場合](#どうすればできるか)
- 指摘やアドバイスありましたら是非コメントをお願いします。必要であれば内容も修正していきます。

# 何をしたいか

私がやっていた作業は、「データベースのデータを全て抜き出す」というものでした。
普通に全てのレコードを取得しようとすると、DBサーバーが"too large"とエラーを返すので、
少しずつ取得してスクリプト側でマージしていく必要がありました。
「取得結果が0件になるまで繰り返しレコードを取得」という処理をしたいわけです。(イメージを下に書いておきます)

```js
let result;
let from = 0;
const count = 1000;
while (true) {
  from += count;
  result = record_shutoku(from, count); // record_shutoku()は単純にレコードの配列を返してくれると想定
  if (result.length == 0) {
    break;
  }
}
```

# 何が問題なのか

`record_shutoku()`みたいな重い処理はJSでは大抵非同期実行になります。今回はこれが`Promise`を返す関数だと想定します。

何回繰り返すのかがわかっていないというのが今回の問題で、
繰り返す回数がわかっていて、並列実行して構わないのであれば`Promise.all()`を使えばOKです。
(今回は深い事情により事前にレコードの件数が取得できず、しかも並列実行もできない状況でした。お金があれば解決できたかもしれません)

また非同期実行関数(`Promise`を返す関数)の同期実行版が用意されているのであれば、`while`とそれを使えば解決です。(上で書いたイメージがまさにそれです)
けど普通は用意されてないことの方が多いと思います。

  - 用意してる例: `fs.readFile()`と`fs.readFileSync()`
  - 用意してない例: `fetch()`

用意してないこと自体は全く問題ないです。むしろ正常な姿だと思います。

さて非同期実行関数を同期的に繰り返すにはどうしたら良いものか。

# async/awaitが使える場合

## async/awaitがあるとあっさり解決

Node.js 8以降を使っていれば簡単に解決する。
`record_shutoku()`が`Promise`を返す関数と想定すると、

```js
async function main() {
  let result;
  let from = 0;
  const count = 1000;
  while (true) {
    from += count;
    result = await record_shutoku(from, count); // record_shutoku()はPromiseを返してくれると想定
    if (result.length == 0) {
      break;
    }
  }
}

main(); // 実行
```

このように書ける。処理全体を`async`関数として作って、`await`を`record_shutoku()`の前につけるだけで良い。
もしエラー処理をしたければ`record_shutoku()`をtry/catchしてあげるだけで良い。

## 確認のために実行できる例

私の場合は[脆弱](https://www.gizmodo.jp/2017/11/patch-of-massive-security-vulnerability-in-high-sierra.html)だったHigh Sierra上で、[asdf](https://github.com/asdf-vm/asdf)とNode.js 8.9.1を使って実行を確認しています。

```js
// 受け取った値 + 1をPromiseで返すだけの関数
// for文は最初に実行されるcalcほど処理を重くするためのもので、本当に同期実行されてるか確認するためのものです。
function calc(num) {
  return new Promise((resolve, reject) => {
    for (let i = 1; i <= 10 - num; i++) {
      console.log(`  calc(${num}): ${i}`);
    }
    return resolve(num + 1);
  });
}

async function main() {
  console.log('start');
  let num = 0;
  while (true) {
    num = await calc(num);
    console.log('main(): result is ' + num); // ここは同期実行できてるか確認するためのものです。
    if (num >= 10) {
      break;
    }
  }
  console.log('done');
}

main();
```

実行すると以下のような出力がされて、同期的に実行できてることがわかる。

```
start
  calc(0): 1
  calc(0): 2
  calc(0): 3
  calc(0): 4
  calc(0): 5
  calc(0): 6
  calc(0): 7
  calc(0): 8
  calc(0): 9
  calc(0): 10
main(): result is 1
  calc(1): 1
  calc(1): 2
  calc(1): 3
  calc(1): 4
  calc(1): 5
  calc(1): 6
  calc(1): 7
  calc(1): 8
  calc(1): 9
main(): result is 2

... # 省略

main(): result is 9
  calc(9): 1
main(): result is 10
done
```

## まとめ1

- async/awaitありがたい
  - 非同期処理の関数を同期的に繰り返すのも簡単
  - try/catchも普通に使える

# async/awaitが使えない場合どうすればいいですか？

今回の私の状況はまさにそれで、Node.js 6を使わなければいけない事態でした。(お金と時間があればNode.js 8にアップデートできたかもしれません)
したがってasync/awaitが使えません。

## とりあえず軽くやり方を探ってみた

検索してみて、見つかったのは以下でした。

- https://stackoverflow.com/questions/29880715/how-to-synchronize-a-sequence-of-promises
- https://stackoverflow.com/questions/28683071/how-do-you-synchronously-resolve-a-chain-of-es6-promises

一番シンプルと思われるものを抜粋

```js
var p = Promise.resolve();
for (let i=1; i<=10; i++) {
    p = p.then(() => promiseReturner(i));
}
```

ただ上記のやり方だと繰り返す回数がわかっているという前提が必要なのと、async/awaitでやっている`break`が実現できない。

## どうすればできるか

以下のように再帰関数を使うと実現できます。(実は独力で解決したわけではなく、弊社のハイパーエンジニアに助けていただきました)

```js
// calc()自体はasync/awaitの例と全く同じ
function calc(num) {
  return new Promise((resolve, reject) => {
    for (let i = 1; i <= 10 - num; i++) {
      console.log(`  calc(${num}): ${i}`);
    }
    return resolve(num + 1);
  });
}

function kurikaeshi_calc(num) {
  if (num >= 10) {
    return Promise.resolve(num);
  } else {
    return calc(num).then((n) => {
      console.log('kurikaeshi_calc(): result is ' + n);
      return kurikaeshi_calc(n);
    });
  }
}

console.log('start');
kurikaeshi_calc(0).then(num => {
  console.log('done');
});
```

出力はasync/awaitのものと同じなので省略。

## 少しだけ解説

処理の本流は

1. `console.log('start')`で文字列出力
2. `kurikaeshi_calc(0)`が`calc(0).then(...)`でできる`Promise`を返す
3. それに対して`.then(num => console.log('done'))`で次の処理を登録

これで終了。
問題は`Promise`の中での処理。

1. `calc(0).then(...)`の`...`で`kurikaeshi_calc(1)`、すなわち`calc(1).then(...)`を返す
2. `calc(1).then(...)`の`...`で`kurikaeshi_calc(2)`、すなわち`calc(2).then(...)`を返す
3. (省略)
4. `calc(9).then(...)`の`...`で`kurikaeshi_calc(10)`、すなわち`Promise.resolve(10)`を返す

上記のように最後以外は **`.then()`によって次の処理を登録済みの`Promise`** を返してくるので、
それら全部の実行が終わるまで`.then(num => console.log('done'))`は待っていることになる。

## まとめ2

- async/awaitありがたい
- 再帰関数でも実はそれなりにシンプルに書ける
  - 関数型プログラミング思考は大事だと再認識
  - エラー処理は普通に`.catch()`すればOK
  - とはいえ(人によっては)ややこしく思える

# `Promise`が使えない場合どうすればいいですか？

頑張ってください。
すいません。特に必要に迫られてないので考えてないです。
たぶん`Promise`の例とそんなに変わらないと思ってます。

# 総括

今回はES7のasync/awaitの便利さを感じるとともに、自分の関数型プログラミング力の甘さを再認識するテーマとなりました。
ただ元も子もないことを言えば、適当なスクリプト書く程度であればそもそも無理してNode.jsを使わなくてもいいんですけどね。

明日は @magicant さんの「cd コマンドの罠」です。罠とはなんだろう...気になる！