[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#util_util_promisify_original

なお、util.promisify以外にもjsでpromisifyするためのパッケージは複数存在しますが、今回は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#util_custom_promisified_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のコードに戻ると、先程のpropertyの値は 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の型推論もいい感じになったらしいので、いいと思います。

感想

正直なところ、使いやすさならutil.promisifyを上回るパッケージはあります(複数個の引数対応やthisなど)が、安心感が欲しいなら十分アリだと思います。しかし、可能ならそもそも元のパッケージをPromise対応させたいところですね。(身も蓋もない話ですが)

スポンサーリンク

シェアする

フォローする