JavaScript/TypeScript で Promise を直列実行する

# 2019/05/19 追記

- パフォーマンス測定した
    - [JavaScript/TypeScript で Promise を直列実行できるが、結局どう実装すればよいのか？（可読性・実行速度）](https://qiita.com/arx8/items/8a9aeeb4f743949087e6)

# 目的

- 複数の `Promise` を直列実行したい
    - 同期的実行・シーケンシャル・シリアルなど呼び方色々あるが、とにかく Not 並列に `Promise[]` を実行したい
- `Promise.all` のような書き心地がいい
- 技術的な正しさを追求した記事ではなく、雰囲気で使いこなせることを優先した記事である

## 動作イメージ

![test.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170621/2b7462ab-d9c9-fa77-1802-31d9591c2c1d.gif)

## この記事に出てくるサンプルコードについて

- `Promise`, `async/await` はなんとなくわかってる人向け
- `axios` で書いている
    - 標準の `fetch` は、ボイラープレートコードが読みづらい・この記事の本質から外れそうなのでやめた
- よって、試す場合は [jsfiddle](https://jsfiddle.net/) などで実行のこと
    - やりすぎると API(GitHub) に対するちょっとしたDOS攻撃になりかねないため、節度を持ってご利用ください
        - そうなる前に rate limit で弾かれるけども

# tl;dr

- https://jsfiddle.net/r7Ljfmbv/

```js:tl;dr
/**
 * @param {(() => Promise<T>)[]} promises
 * @returns {Promise<T[]>}
 * @template T
 */
const sequential = async (promises) => {
  const first = promises.shift()
  if (first == null) {
    return []
  }

  const results = []
  await promises
    // 末尾に空のPromiseがないと、最後のPromiseの結果をresultsにpushできないため
    .concat(() => Promise.resolve())
    .reduce(async (prev, next) => {
      const res = await prev
      results.push(res)
      return next()
    }, Promise.resolve(first()))

  return results
}

// 使う側
const main = async () => {
  const promises = [
    () => axios.get("https://api.github.com/search/users?q=siro"),
    () => axios.get("https://api.github.com/search/users?q=yamato"),
    () => axios.get("https://api.github.com/search/users?q=kiso"),
  ]

  const results = await sequential(promises)

  console.log(results)
}

main()
```

- 以下、上記コードに至るまでの過程

# 実装過程

## Lv.1 : べた書き

- まぁそりゃできるでしょうね

```js:Lv.1
const main = async () => {
  const results = []
  results.push(await axios.get("https://api.github.com/search/users?q=siro"))
  results.push(await axios.get("https://api.github.com/search/users?q=yamato"))
  results.push(await axios.get("https://api.github.com/search/users?q=kiso"))

  console.log(results)
}

main()
```

## Lv.2 : for-of パターン

- `eslint-config-airbnb` に怒られるやつ
    - ちなみにそのメッセージは「iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations.」
    - 要するに「変換に必要なruntimeが重い」らしい
- `results.push` で mutable なのが残念だが、割と誰でも読みやすい・直感的だと思うので嫌いじゃない

```js:Lv.2
const main = async () => {
  const promises = [
    () => axios.get("https://api.github.com/search/users?q=siro"),
    () => axios.get("https://api.github.com/search/users?q=yamato"),
    () => axios.get("https://api.github.com/search/users?q=kiso"),
  ]

  const results = []
  for (const p of promises) {
    results.push(await p())
  }

  console.log(results)
}

main()
```

## Lv.2- : ダメなパターン Array​.prototype.forEach

- これは並列実行になってしまう
- `forEach` の処理自体は `await` されず実行され、 `results.push` だけが `await` 後に実行される

```js:Lv.2-
const main = async () => {
  const promises = [
    () => axios.get("https://api.github.com/search/users?q=siro"),
    () => axios.get("https://api.github.com/search/users?q=yamato"),
    () => axios.get("https://api.github.com/search/users?q=kiso"),
  ]

  const results = []
  promises.forEach(async (p) => {
    results.push(await p())
  })

  console.log(results)
}

main()
```

- ちょっと Babel(トランスパイラ) の気持ちになってみよう
- `async/await` を `then` に変換する場合、きっとこんなコードになるはずだ
- これなら確かに並列実行になるだろうと予想できる

```js:Lv.2-_kimoti
const main = async () => {
  const promises = [
    () => axios.get("https://api.github.com/search/users?q=siro"),
    () => axios.get("https://api.github.com/search/users?q=yamato"),
    () => axios.get("https://api.github.com/search/users?q=kiso"),
  ]

  const results = []
  promises.forEach((p) => {
    p().then((res) => {
      results.push(res)
    })
  })

  console.log(results)
}

main()
```

## Lv.2-- : ダメなパターン Array​.prototype.map

- そもそもこれでは `results` の型が `Promise<AxiosResponse>[]` になるので、全然ダメ

```js:Lv.2--
const main = async () => {
  const promises = [
    () => axios.get("https://api.github.com/search/users?q=siro"),
    () => axios.get("https://api.github.com/search/users?q=yamato"),
    () => axios.get("https://api.github.com/search/users?q=kiso"),
  ]

  const results = promises.map(async (p) => {
    return await p()
  })
  // Babel の気持ち
  // const results = promises.map(async (p) => {
  //   return p().then()
  // })

  console.log(results)
}

main()
```

## Lv.3 : reduce パターン

- ググるとよく出てくるやつ

```js:Lv.3
const main = async () => {
  const promises = [
    () => axios.get("https://api.github.com/search/users?q=siro"),
    () => axios.get("https://api.github.com/search/users?q=yamato"),
    () => axios.get("https://api.github.com/search/users?q=kiso"),
  ]

  const results = []
  await promises.reduce((prev, next) => {
    return prev.then((result) => {
      results.push(result)
      return next()
    })
  }, Promise.resolve())

  console.log(results)
}

main()
```

- VSCodeを使っていれば、「This may be converted to an async function. ts(80006)」と指摘され、QuickFixで async/await に自動変換できるはず
- `then` が消えて少し読みやすくなる

```js:Lv.3_async
const main = async () => {
  const promises = [
    () => axios.get("https://api.github.com/search/users?q=siro"),
    () => axios.get("https://api.github.com/search/users?q=yamato"),
    () => axios.get("https://api.github.com/search/users?q=kiso"),
  ]

  const results = []
  await promises.reduce(async (prev, next) => {
    const res = await prev
    results.push(res)
    return next()
  }, Promise.resolve())

  console.log(results)
}

main()
```

- 写経すると `reduce` の中の `return` や `()` を書き忘れてうまく動かなかったり、コードが理解できなくてハマる
- 初めて見た時は「なんで reduce で直列実行になるんだ？」と理解に苦しんだ・・・

## Lv.3+ : reduce パターン改

- 残念ながら、上記の「Lv.3」のコードには2点問題がある
    1. `reduce` の initialValue の Promise の `resolve` 結果が空(undefined)
    2. `promises[2]` の結果が push されない
- よって、`results` の期待値と実際の結果が異なる
    - 期待値 : `[ AxiosResponse(siro), AxiosResponse(yamato), AxiosResponse(kiso) ]`
    - 実結果 : `[ undefined, AxiosResponse(siro), AxiosResponse(yamato) ]`
- 「3つの非同期処理を直列に実行する」だけならこれでもよいが、結果が受け取りたい場合はもう一工夫必要なようだ
- 上記問題を解消してみる

```js:Lv.3+
const main = async () => {
  const promises = [
    () => axios.get("https://api.github.com/search/users?q=siro"),
    () => axios.get("https://api.github.com/search/users?q=yamato"),
    () => axios.get("https://api.github.com/search/users?q=kiso"),
  ]

  const results = []
  await promises
    .concat(() => Promise.resolve())
    .reduce(async (prev, next) => {
      const res = await prev
      // reduce の initialValue の resolve 結果を、結果配列に push させないため undefined チェック
      if (res != null) {
        results.push(res)
      }
      return next()
    }, Promise.resolve())

  console.log(results)
}

main()
```

- うーん。汚い（白目）
    - `// @ts-check` を使ってると型推論結果がうまく合わないのか、エラーが出て更にストレスマッハ
    - `reduce` 使ってるのに `results.push` が必要で mutable になっているのもダサい
- そもそもなぜ「問題2」が `.concat(() => Promise.resolve())` で解決できるのか？
    - これを理解するため、ここで JavaScript の気持ちになって `reduce` をバラしてみる

```js:Lv.3+_kimoti
const main = async () => {
  const promises = [
    () => axios.get("https://api.github.com/search/users?q=siro"),
    () => axios.get("https://api.github.com/search/users?q=yamato"),
    () => axios.get("https://api.github.com/search/users?q=kiso"),
  ]
  .concat(() => Promise.resolve())

  const results = []

  // 1ループ目
  const res = await Promise.resolve()
  if (res != null) {
    results.push(res)
  }
  const next0 = promises[0]() // siro

  // 2ループ目
  const res0 = await next0
  if (res0 != null) {
    results.push(res0)
  }
  const next1 = promises[1]() // yamato

  // 3ループ目
  const res1 = await next1
  if (res1 != null) {
    results.push(res1)
  }
  const next2 = promises[2]() // kiso

  // 4ループ目
  const res2 = await next2
  if (res2 != null) {
    results.push(res2)
  }
  const next3 = promises[3]() // .concat(() => Promise.resolve())

  console.log(results)
}

main()
```

- 「＊ループ目」(`reduce` の中身の処理) は `promises.length` 回分実行される
- よって、下記のようになり「問題2」が解決できる
    - `promises.length` = 3 だった `promises` に `.concat(() => Promise.resolve())` で 1回分ループを足す
    - ＝ 「4ループ目」が実行される
    - ＝ `const next2 = promises[2]() // kiso` の await 後の結果の `push` が実行される
- さらに Babel の気持ちになって async/await を then に・・・は面倒くさいので省略

## Lv.3++ : reduce パターン改二

- `results` を作ってる処理を function `sequential` に切り出す
- 切り出すことで、 mutable な処理は `sequential` に押し込まれ、メイン処理から見れば `results` は immutable となった
- immutable 万歳！

```js:Lv.3++
const sequential = async (promises) => {
  const results = []

  await promises
    .concat(() => Promise.resolve())
    .reduce(async (prev, next) => {
      const res = await prev
      // reduce の initialValue の resolve 結果を、結果配列に push させないため undefined チェック
      if (res != null) {
        results.push(res)
      }
      return next()
    }, Promise.resolve())

  return results
}

const main = async () => {
  const promises = [
    () => axios.get("https://api.github.com/search/users?q=siro"),
    () => axios.get("https://api.github.com/search/users?q=yamato"),
    () => axios.get("https://api.github.com/search/users?q=kiso"),
  ]

  const results = await sequential(promises)

  console.log(results)
}

main()
```

## Lv.Final

- だいぶマシにはなったが、まだ null チェックが目障り
- 無駄な処理を省く・整理する

```js:Lv.Final
const sequential = async (promises) => {
  const first = promises.shift()
  if (first == null) {
    return []
  }

  const results = []

  await promises
    // 末尾に空のPromiseがないと、最後のPromiseの結果をresultsにpushできないため
    .concat(() => Promise.resolve())
    .reduce(async (prev, next) => {
      const res = await prev
      results.push(res)
      return next()
    }, Promise.resolve(first()))

  return results
}

const main = async () => {
  const promises = [
    () => axios.get("https://api.github.com/search/users?q=siro"),
    () => axios.get("https://api.github.com/search/users?q=yamato"),
    () => axios.get("https://api.github.com/search/users?q=kiso"),
  ]

  const results = await sequential(promises)

  console.log(results)
}

main()
```

- 改良の余地はあるかもしれないが、とりあえずこれで目的達成

## Promise.all との比較

- 完全に同じシグネチャにはできなかった
    - 宣言した時点 (`axios.get("...")` の時点) で各非同期処理が開始され、結果的に並列実行になるため
    - 直列にしたい場合は `function` で包んで、その `function` が実行されるまで `Promise` が発火しないよう実装する必要があるため

```js:Promise.all
const main = async () => {
  const promises = [
    axios.get("https://api.github.com/search/users?q=siro"),
    axios.get("https://api.github.com/search/users?q=yamato"),
    axios.get("https://api.github.com/search/users?q=kiso"),
  ]

  const results = await Promise.all(promises)

  console.log(results)
}

main()
```

## TypeScript + UnitTest

- 下記リポジトリを参照のこと
    - https://github.com/arx-8/try-sequential-promises

(TS + Jest の実行環境を簡単に用意するため、「`create-react-app` で生成 → 不要なもの削除」で作っている。少し冗長だがご容赦を)

# まとめ

- いちいち定義するのめんどくさいので標準化されないかなー
- 気が向いたらもうちょい改良して npm package 化してみようと思う