JSの新機能、Async Iterationの使いどころ

非同期反復(async iteration)が年初にStage 4入りし、[ES2018に追加されそう](http://2ality.com/2017/02/ecmascript-2018.html)な見込みです。「先にもっと他のを!」という人もいそうですが、何が美味しいのか、少し書いてみたいと思います。

- [Async Iteration プロポーザル](https://github.com/tc39/proposal-async-iteration) (参考: [審議中](https://github.com/tc39/proposals) / [Stage 4](https://github.com/tc39/proposals/blob/master/finished-proposals.md))

## どんなやつ?

実際に使うには、非同期ジェネレータを定義して、`for await (...)`構文で呼び出します。(他の書き方もありますが、使用頻度が低そうなので省略)

```javascript
// 指定のミリ秒待つ非同期関数 sleep(msec) が定義済みとして...

async function* wakeUp () { // 「*」を付けて、ジェネレータであることを示す
  yield 'Zzzz...' // ジェネレータなので、yieldが使える
  await sleep(2000) // asyncなので、awaitが使える
  yield 'Mornin!' // ジェネレータは複数回、結果を返せる
}
```

これは、次のように呼び出せます。

```javascript
async function main () {
  for await (const message of wakeUp()) {
    console.log(message)
  }
}
main()
```

上記を実行すると、即座に'Zzzz...'と返ったあと、2000ミリ秒待って'Mornin!'が返ります。なお、`for await`も非同期なので、使えるのは`async`な関数の中だけです。

## 誰が使うのか?

(Asyncではない)ジェネレータについては、ライブラリの製作者でもないかぎり、日常的に使うものではありませんでした。特に`async/await`の構文がES2017に入って以来、ユーザがジェネレータについて意識するシーンは激減したように思います。

一方、非同期ジェネレータは、ジェネレータというより **非同期関数の延長** として、捉えたほうが良さそうです。理由(というか使いとごろ)を次節に書きますが、実はむしろユーザサイドで使うと便利な機能です。(少なくとも筆者の所感としては)

## 使いどころ

説明のため、`fetch`で2つのデータを取得する非同期関数を考えます。ジェネレータにしなくても、次のように書くことは可能です。

```javascript
// 非同期関数のみ
async function getData () {
  const data1 = await (await fetch(url1)).json()
  const data2 = await (await fetch(url2)).json()
  return {data1, data2}
}
```

ここで、途中の取得状況を示すにはどうしましょうか? これは結構厄介な問題で、ステータス更新の関数を途中に挟むなどの必要が生じます。しかし、この書き方ではあっという間に複雑化してしまいそうです。(ロジックの分割の点で不利)

```javascript
// 複雑化してしまう...
async function getData () {
  updateStatus('0/2')
  const data1 = await (await fetch(url1)).json()
  updateStatus('1/2')
  const data2 = await (await fetch(url2)).json()
  updateStatus('2/2')
  return {data1, data2}
}
```

ここで、非同期ジェネレータを使うと、シンプルに書けるようになります。

```javascript
// 非同期ジェネレータで、すっきり
async function* getData () {
  yield {status: '0/2'}
  const data1 = await (await fetch(url1)).json()
  yield {status: '1/2', data1}
  const data2 = await (await fetch(url2)).json()
  yield {status: '2/2', data2}
}
```

こう書きたかったシーン、UI周りでは結構多かったはず。領域として、[redux-saga](https://redux-saga.js.org/)で無理くりやっていたあたりが、JavaScriptの基本機能で書けるようになる感じでしょうか。(大雑把な話としては)

> **メモ**: 参考まで、検索したら、Reduxのミドルウェアがありました。[redux-thunk-generators](https://github.com/doasync/redux-thunk-generators)

## 使い方入門

### クラスやオブジェクト内で書く

独立した関数として書くなら、`await`をつけて`function`の後にアスタリスク:

```javascript
await function* simple () {
  ...
}
```

インスタンスメソッドとして書くなら、メソッド名の前にアスタリスク:

```javascript
class Something {
  await *load () {
    ...
  }
  await *save () {
    ...
  }
}
```

オブジェクトメソッドとして書くなら:

```javascript
const something = {
  await *load () {
    ...
  }, // ここのカンマを忘れずに
  await *save () {
    ...
  }
}
```

### 並列と直列

複数の非同期関数があって、同時に実行する(並列)、あるいは順番に実行する(直列)ケースを考えます。

```javascript
const promises = [
  Promise.resolve('a'),
  Promise.resolve('b'),
  Promise.resolve('c')
]
```

並列のケースは以前から`Promise.all()`で書けました。

```javascript
async function main () {
  const results = await Promise.all(promises)
  for (const result of results) {
    // do something on results
  }
}
main()
```

一方、直列についてキレイな書き方がなかったのですが、非同期ジェネレータで扱いやすくなります。

```javascript
async function* series (promises) {
  for (const p of promises) yield await p
}

async function main () {
  for await (const result of series(promises)) {
    // do something on results
  }
}
main()
```

### 非同期ジェネレータを見分ける

次のような3つの関数があったとします:

```javascript
function fA () {...}
await function fB () {...}
await function* fC () {...}
```

非同期ジェネレータであるかは、次のコードで見分けられます。

```javascript
const result = fC() // 非同期ジェネレータは、実行すると非同期イテレータを返す
const isAsyncIterable = typeof result[Symbol.asyncIterator] === 'function'
```

非同期関数は、次のコードで。

```javascript
const result = fB() // 非同期関数は、実行するとプロミスを返す
const isPromise = typeof result.then === 'function'
```

残りが普通の関数です。

| 種類 | 例 | 戻り値 |
|:--|:--|:--|
| 関数 | function fA () {...} | 値 |
| 非同期関数 | await function fB () {...} | プロミス |
| 非同期ジェネレータ | await function* fC () {...} | 非同期イテレータ |

### プリコンパイル

2018年4月現在のサポート状況は、次の通りです。[詳しくはこちら](http://kangax.github.io/compat-table/es2016plus/#test-Asynchronous_Iterators)。

- Google Chrome 63 以降
- Firefox 57 以降
- Safari Technical Preview 以降

つまり、しばらくはBabelとプラグインが必要です。

- [transform-async-generator-functions](https://babeljs.io/docs/plugins/transform-async-generator-functions/)

参考まで、Rollupする場合は、このあたり↓をざくっとインストールしておきます。おそらく、`external-helpers`と`polyfill`も必要になりますが、それぞれの環境に合わせてください。

```bash
npm i -D \
  babel-core \
  babel-preset-env \
  babel-plugin-external-helpers \
  babel-polyfill \
  babel-plugin-transform-async-generator-functions \
  rollup \
  rollup-plugin-babel \
  rollup-plugin-commonjs \
  rollup-plugin-node-resolve
```

以下は`rollup.config.js`の例です。

```javascript
import resolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
import babel from 'rollup-plugin-babel'

export default {
  input: 'main.js',
  plugins: [
    resolve(),
    commonjs(),
    babel({
      babelrc: false,
      presets: [['env', {modules: false}]],
      plugins: [
        'transform-async-generator-functions',
        'external-helpers'
      ]
    })
  ],
  output: {
    file: 'bundle.js',
    format: 'iife',
    sourcemap: true
  }
}
```

## Observableとともに

本格的に使うならおそらく必要になるので、Observableとの合わせ技をまとめます。

### Observableとの互換性

非同期関数とプロミスの関係はシンプルでした。

```javascript
async function asyncFunc () {...}
const promise = asyncFunc()
```

一方、非同期ジェネレータとプロミスの関係は一段遠くなります。

```javascript
async function* asyncGenFunc () {...}
const asyncIterator = asyncGenFunc()
const promise = asyncIterator.next() // nextはプロミスを返す
```

プロミスの「約束」は一回限りですから、非同期イテレータの返す系列全体を扱うことはできません。複数回の「約束」に対応した概念としてObservableがあります。Observable自体の説明は省きますが、以下参考:

- [Observable](https://github.com/tc39/proposal-observable)はプロポーザルとしては現在Stage 1
- 今のところ、[zen-observable](https://github.com/zenparsing/zen-observable)ほかのライブラリが必要

非同期イテレータは次のように、Observableに変換が可能です。

```javascript
/** 非同期イテレータ → Observable */
function toObservable(asyncIterator) {
  return new Observable(async observer => {
    try {
      for await (const data of asyncIterator) {
        if (data) observer.next(data)
      }
    } catch (e) {
      observer.error(e)
    }
    observer.complete()
  })
}
```

非同期ジェネレータは「pull」ですが、「push」で(イベント駆動的に)使いたいときはこのようにObservableに変換すると便利です。

```javascript
async function* wakeUp () {
  yield 'Zzzz...'
  await sleep(2000)
  yield 'Mornin!'
}
toObservable(wakeUp())
  .map(msg => msg.toUpperCase()) // 大文字に
  .subscribe(msg => console.log(msg)) // ZZZZ..., MORNIN!
```

なお、時系列方向の拡がりと、push/pullで分類すると次のようになります。

| 「約束」 | Pull | Push |
|--:|:--|:--|
| 1回 | 非同期関数 | Promise |
| N回 | 非同期ジェネレータ | Observable |

### 透過的に扱う

ここまでの話をすべて使うと、関数/非同期関数/非同期ジェネレータを透過的に扱うことも可能です。例えば、ユーザとしては、次のようなアクションを同列に扱いたいわけです。

```javascript
const actions = {
  // 関数
  countUp (state) {
    return {count: state.count + 1}
  },
  // 非同期関数
  async countUp2 (state) {
    await sleep(2000)
    return {count: state.count + 1}
  },
  // 非同期ジェネレータ
  async *countUp3 (state) {
    yield {waiting: true}
    await sleep(2000)
    yield {waiting: false, count: state.count + 1}
  }
}
```

現時点でも、非同期関数(プロミス)に対応したライブラリは多いです。また、関数が渡されても非同期関数が渡されても「よしなに」扱ってくれる、つまり **透過的に** 扱うライブラリも増えてきました。

それらと同様に、非同期ジェネレータまで扱えるようにすると、いろいろ表現力が一気に増します。(これ、今後、Reduxのような状態とアクションを扱うライブラリを開発する際には、重要になるんじゃないかと)

さっきの`toObservable()`を拡張して、「透過的な」バージョンを作ってみましょう。

```javascript
/** 値 or プロミス or 非同期イテレータ → Observable */
function toObservable(result) {
  return new Observable(async observer => {
    try {
      const isAsyncIterable = typeof result[Symbol.asyncIterator] === 'function'
      if (isAsyncIterable) {
        for await (const data of result) {
          if (data) observer.next(data)
        }
      } else {
        const data = result.then ? await result : result
        if (data) observer.next(data)
      }
    } catch (e) {
      observer.error(e)
    }
    observer.complete()
  })
}
```

これで、なんでもObservableで待ち受けられるようになりました。

> **追記**: 同様の内容のライブラリを公開しました。
> https://github.com/cognitom/fafgag

## まとめ

便利。