03/31/2019, 8:20 PM GMT+9

[JavaScript] callbackではなくPromiseを使うように変換する util.promisify

callback と Promise

JavaScript で非同期な処理を行う関数を実現するために、今では Promise を使うのが一般的ですが、いくつかの古いライブラリは Promise ではなく callback 関数を用いた方法にしか対応していないことがあります。元のライブラリ自体は触らずに Promise を使った非同期処理をしたい場合は、その使いたい関数を Promise を返すような関数で wrap するという方法 ( promisification ) があります。

util.promisify の基本的な使い方

node の util モジュールにある promisify (以下 util.promisify )を使うと簡単に promisify できます。元の関数が要求する callback 関数が (err, value) => … という関数で、それが元の関数の引数の一番最後で渡すように定義されている極めて一般的な関数(引数・戻り値の意味で)の場合は、 promisified_func = util.promisify(hoge_func); とした後、 await promisified_func(arg1, arg2); とするだけで、promisification が完了します。このような基本的な使い方については、ドキュメントを読んでいただいたほうが分かりやすいのでそちらに任せます。。
https://nodejs.org/docs/latest-v8.x/api/util.html#utilutilpromisify_original

なお、util.promisify 以外にも promisification のためのパッケージは複数存在しますが、今回は util.promisify を使う方向で話を進めます。

少し深く理解する

前述の通り、極めて一般的な関数の場合、util.promisify がよしなにしてくれます。しかし、そこから外れると少し面倒なことになるのです。今回は、実際のコードを見ながら次のような場合について考えます。

  • callback 関数を受け取る引数の位置が最後でない場合
  • callback 関数が受け取る引数が (err, value)ではない場合
  • メソッドを promisify したい場合

コードを読む

まずは util.promisify のコードを軽く読んでみましょう。次のリンクから確認できます。(リンクは記事作成当時の最新) https://github.com/nodejs/node/blob/ed5e69d7e6eaf5e887ff48aa3fe941d394c44846/lib/internal/util.js#L265-L315

まずは通常のフロー、つまり、util.promisify が全部やってくれるときの処理を追ってみましょう。何も指定していない関数には kCustomPromisifiedSymbolkCustomPromisifyArgsSymbol といった property(詳細は後述)は存在しないので、その分岐を飛ばして、L.286-以降を見ていきましょう。

L.286-302 の fn は closure で、冒頭に述べたように、元の関数を wrap して Promise を返し、引数の callback で resolve や reject をしています。L.304-312 では、closure である fn に元の関数の prototype や property をつけてから、fn を返しています。L.315 は、kCustomPromisifiedSymbol を promisify.custom として export するためのものです。(詳細は後述)

callback 関数を受け取る引数の位置が最後でない場合

問題になりそうなのはL.288 の部分です。そして残念なことに、 (err, ...values) の位置を変えるオプションはなさそうです。よって、自分で fn から定義する必要があります。

公式ドキュメントの https://nodejs.org/docs/latest-v8.x/api/util.html#utilcustompromisified_functions では、
util.promisify.custom を使えば良いと書かれています。 L.315 より、これは kCustomPromisifiedSymbol という Symbol で、これが定義されているとき promisify は、L.272-280のように、その関数をほぼそのまま返すことが分かります。

このような場合、wrap する関数をほぼ全て自分で書くことになるので、util.promisify を使うメリットはあまりなさそうです。他の関数について util.promisify を使っていて、揃えたい場合には良いかと思います。

callback 関数の引数が違う場合

callback 関数が、例えば (err, value1, value2) => {... の場合、util.promisify は resolve 時に value1 しか返してくれません。(L.298)

しかし、一部の node の関数、例えば fs.read()などは、callback 関数の引数に(err, bytesRead, buffer) のように 3 つ以上の引数を渡します。このような関数はどのようにこの問題を解決しているのでしょうか。 答えは https://github.com/nodejs/node/blob/39141426d46a7e55d93ad2e8efa12ed86d223522/lib/fs.js#L492-L493 にありました。どうやら関数customPromisifyArgsという property を定義しているようです。

promisify のコードに戻ると、先程の customPromisifyArgs の値は argumentNames に代入されているようです。ということは、argumentNames に ['value1', 'value2'] を渡せれば {value1: 'hoge', value2: 'piyo'} みたいに渡してくれるのですが、そのためには元の関数にkCustomPromisifyArgsSymbolを key に持つ property を付与する必要があり、こちらは kCustomPromisifiedSymbol とは違い export されていません。(internal までしか見えない)

これは意図的で、 promisify.custom で十分だと考えたようです。 (どうしてもというなら、少し強引なやり方はあるようです)

したがって、この場合も、 util.promisify.custom を使ってほぼ全てのコードを書くことになり、 util.promisify の恩恵を受けることはできなさそうです。

メソッドの promisify

これは promisify の話というよりthis の話です。

https://github.com/nodejs/node/issues/13338 で util.promisify に this context の option をつけるべきかが議論されていますが、現在はclosed になっています。ということで、bind を使うしかなさそうです。Typescript の型推論が気になりますが、https://qiita.com/vvakame/items/79557e00cfe6d3c612cd によると、bind の型推論も改善されたようなので問題なさそうです。

まとめ

  • Javascriptでcallbackを要求するような非同期関数をasync関数にすることをpromisifyという
  • util.promisifyを使うとpromisifyを簡単に行うことができる
  • ただし引数の最後にcallbackがない、あるいはcallbackの引数が(err, value)でない場合は、util.promisifyを使うメリットは限定的(ほとんど自分で書くのと変わりない)

Cosnomi
Cosnomi

コンピュータ(Web, 機械学習など)が好きな医学部生

Twitter / GitHub