非同期関数を同期関数のように扱えるawait機構をさわってみた

## イントロ

非同期化された関数を同期的に使用できた方がコードの見通しがよくなることが結構ありますが、そのように**「非同期関数を動機的に処理」**するための ``await``機構についての整理。。ついでに関数を非同期化する機構である``Promise``や``async``などについて。

実行環境はなぜか Cloud Functions for Firebase のローカル環境ですが、特にFunctionsである必要性はありません。
たまたまココで作業してて、そのままその環境で稼働確認してるだけ、、です。


環境の準備は、[Cloud Functions for Firebase をFirebase Hostingへデプロイするための環境構築手順](https://qiita.com/masatomix/items/bd1c3a6b31e8d2cb7aef) このあたりのFunctionsの構築手順をやればよろしいかと思います。

## やってみる

### まずは初期状態

まず、時間がかかる``execute``関数があるとします。

```typescript:utils.ts
const me = {
  /**
   * もとの、時間がかかる関数。
   * @param num
   */
  execute (num) {
    // 時間がかかる(5秒)処理
    // console.log('execute start.')
    this.sleep(5000)
    // console.log('execute 5秒経過')
    if (num == 100) {
      // console.log('execute エラー終了')
      throw new Error('100だけダメっていう関数なんです')
    }
    // console.log('execute 正常終了')
    return num * 10
  },

  // https://qiita.com/albno273/items/c2d48fdcbf3a9a3434db 感謝!(おもいっぽいので実用上はご注意)
  sleep (time) {
    const d1: any = new Date()
    while (true) {
      const d2: any = new Date()
      if (d2 - d1 > time) {
        return
      }
    }
  }
}

export default me

if (!module.parent) {
  console.log(me.execute(10))
}
```

Functionsを作成します。

```typescript:index.ts
import * as functions from 'firebase-functions'
import utils from './utils'

export const helloWorld = functions.https.onRequest((request, response) => {
  const num = request.query.num

  try {
    const result = utils.execute(num)
    console.log(result)
    response.send(`${result}`)
  } catch (error) {
    console.log(error.message)
    response.send(error.message)
  }
  console.log('End.')
})
```

ビルドしてローカルでFunctionsを起動します。

```
$ cd functions/ && npm run build && cd ../
$ firebase serve --only functions
```

別のターミナルからcurlで関数を呼び出してみます。たとえば下記の通り。

```
$ curl http://localhost:5000/fb-samples-xxxx/us-central1/helloWorld -d 'num=50' -G
500
$ curl http://localhost:5000/fb-samples-xxxx/us-central1/helloWorld -d 'num=100' -G
100だけダメっていう関数なんです
$
```

5秒たってたから、処理が返ってきましたね。コンソールを見てみると

```
info: User function triggered, starting execution
info: execute start.
info: execute 5秒経過
info: execute 正常終了
info: 500
info: End.
info: Execution took 5019 ms, user function completed successfully

info: User function triggered, starting execution
info: execute start.
info: execute 5秒経過
info: execute エラー終了
info: 100だけダメっていう関数なんです
info: End.
Execution took 5005 ms, user function completed successfully
```

うん、上から順番に同期的に順次実行されていますね。


### Promiseで非同期化する

さて、遅い``execute``関数をPromise機構を使って非同期化します。``Promise``はデザインパタンでいうFutureパターンのように、とりあえずなにかしらのチケット(Promise)を返却し、処理が完了したらそのチケットにあらかじめ登録しておいた関数をコールバックするような方式です。[^1]
そのコールバックされる関数の引数に、実行結果が載せられてくるようになっています。

[^1]: Futureパタンは別スレッドでデータを取得させておいて、ホントに必要になったときに ``Future#getResult()``をよびだして実データをもらう(まだならさすがにそこでwaitさせる) ってので、結果を取得する方式が微妙に異なりますが、、。

さて``Promise``を使うとこうなります。

```typescript:utils.ts
const me = {
  /**
   * オリジナルの関数をPromiseで非同期化した
   * @param num
   */
  asyncExecute (num) {
    return new Promise((resolve, reject) => {
      try {
        const result = this.execute(num)
        resolve(result)
      } catch (error) {
        reject(error)
      }
    })
  },
  ... 以降はおなじなので割愛
```

もとの関数が正常終了したら ``resolve``関数を呼び出して、実行結果をセットしています。
もとの関数が異常終了したら、``reject`` 関数を呼び出して、errorインスタンスをセットしています。

非同期化した関数を作成しました。この``Promise``を返す関数は、下記のように使用します。


```typescript:index.ts
import * as functions from 'firebase-functions'
import utils from './utils'

export const helloWorld = functions.https.onRequest((request, response) => {
  const num = request.query.num

  const resultPromise = utils.asyncExecute(num)
  resultPromise
    .then(result => {
      console.log(result)
      response.send(`${result}`)
    })
    .catch(error => {
      console.log(error.message)
      response.send(error.message)
    })
  console.log('End.')
})
```
``.then``,``.catch``にコールバックしてほしい関数をセットしています。

ビルドしてローカルで起動して、curlしてみたときのコンソールに出力される内容は以下の通り

```
info: User function triggered, starting execution
info: execute start.
info: execute 5秒経過
info: execute 正常終了
End.
info: 500
info: Execution took 5019 ms, user function completed successfully

info: User function triggered, starting execution
info: execute start.
info: execute 5秒経過
info: execute エラー終了
info: End.
info: 100だけダメっていう関数なんです
info: Execution took 5023 ms, user function completed successfully
```

うん、実行結果より先にEndが返ってきています。ホントは5秒待つことなくEndが出力されると思ったのですが、、どうやら待っちゃってますね、、なんでかな、、。


### asyncで非同期化する

さて先ほどは、非同期化するための``Promise``を返す関数でラップしましたが、``async``機構を使うと、そのラッピングを自動化してくれます。具体的には ``execute`` 関数に``async``をつけるだけです。(ついでに``asyncExecute``は不要なので削除)

```typescript:utils.ts
const me = {
  /**
   * もとの時間がかかる関数。
   * @param num
   */
  async execute (num) {
    // 時間がかかる(2秒)処理
    console.log('execute start.')
    this.sleep(5000)
    console.log('execute 5秒経過')
    if (num == 100) {
      console.log('execute エラー終了')
      throw new Error('100だけダメっていう関数なんです')
    }
    console.log('execute 正常終了')
    return num * 10
  },

  // https://qiita.com/albno273/items/c2d48fdcbf3a9a3434db
  sleep (time) {
    const d1: any = new Date()
    while (true) {
      const d2: any = new Date()
      if (d2 - d1 > time) {
        return
      }
    }
  }
}

export default me

if (!module.parent) {
  console.log(me.execute(10))
}
```

``async``をつけると、その関数が``Promise``インスタンスを返すようになってくれるそうで、従って呼び出し側は、同期関数だった``execute``を以下のように**非同期関数として**あつかうことができるようになります。

```typescript:index.ts
import * as functions from 'firebase-functions'
import utils from './utils'

export const helloWorld = functions.https.onRequest((request, response) => {
  const num = request.query.num

  const resultPromise = utils.execute(num)
  resultPromise
    .then(result => {
      console.log(result)
      response.send(`${result}`)
    })
    .catch(error => {
      console.log(error.message)
      response.send(error.message)
    })
  console.log('End.')
})
```

なるほど。正常終了も異常終了も、さきほどの``asyncExecute``と全く同じようにあつかうことが出来るみたいです。。``async``便利だ、、、。


### 呼びだし元のコードをawaitで同期っぽく書く

さてさて、ようやく``await``機構ですが、``await``は``Promise``インスタンスを返すように記述された関数だったり ``async``で修飾された関数だったり、とにかく非同期の関数について、終了するまでそこで待ってくれるという機構なんだそうです。さきの``async``された``execute``関数を下記のように**同期関数のように**呼び出す事が出来ます。

```typescript:index.ts
import * as functions from 'firebase-functions'
import utils from './utils'

export const helloWorld = functions.https.onRequest( async (request, response) => {
    const num = request.query.num

    try {
      const result = await utils.execute(num)
      console.log(result)
      response.send(`${result}`)
    } catch (error) {
      console.log(error.message)
      response.send(error.message)
    }
    console.log('End.')
  }
)
```

いちばん最初の同期関数``execute``を普通に呼び出したときとほとんどおなじ書き方で、非同期関数を呼ぶことが出来ることがわかると思います。
**「非同期関数に ``await``をつけること」** と **「``await``は``async``な関数内でしか使用できないこと」** がルールです。

一応ですが実行結果は、、、

```
info: User function triggered, starting execution
info: execute start.
info: execute 5秒経過
info: execute 正常終了
info: 500
info: End.
Execution took 5020 ms, user function completed successfully

info: User function triggered, starting execution
info: execute start.
info: execute 5秒経過
info: execute エラー終了
info: 100だけダメっていう関数なんです
info: End.
Execution took 5009 ms, user function completed successfully
```

Endの表示がちゃんと非同期関数の戻りを待ってることがわかります。
ほぼ一番最初のコードに戻りました。自分で非同期化した関数を同期ぽく呼び出す、ってなにやってんのって感じですが、仕組みは理解できました。

## まとめると

- ある関数を``Promise``を返す関数でラップすることで、非同期的に使用することが可能になる
- ``async``機構を使うことで、同期関数 → 非同期関数 に変更する事が出来る(Promise化できる)
- ``await``機構を使うことで、非同期関数 → 同期関数 のように取り扱うことが出来る
- その際は``async``で全体をくるむ必要がある

以上、おつかれさまでした。

## 関連リンク

- [async/await 入門（JavaScript）](https://qiita.com/soarflat/items/1a9613e023200bbebcb3)
- [Promiseの使い方、それに代わるasync/awaitの使い方](https://qiita.com/suin/items/97041d3e0691c12f4974)