JavaScriptの非同期処理ではPromiseを使うのが一般的ですが、古いライブラリはcallback関数を用いた方法にしか対応していないことがあります。元のライブラリのコードを変えずに、Promiseを使った非同期処理をしたい場合は、その使いたい関数をPromiseを返すような関数で包む方法 ( 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以外にも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が全部やってくれるときの処理を追ってみましょう。何も指定していない関数に kCustomPromisifiedSymbol
や kCustomPromisifyArgsSymbol
といったproperty(この2つについては後ほど後述)は存在しないので、その分岐を飛ばして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を使うメリットはあまりなさそうです。
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']
を渡せればpromisyした関数は {value1: 'hoge', value2: 'piyo'}
のように返してくれます。しかし、そのためには元の関数に kCustomPromisifyArgsSymbol
をkeyに持つpropertyを付与する必要があり、こちらは kCustomPromisifiedSymbol
とは違いexportされていないため、私達が使うことは出来ません。これは意図的で、 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を使うメリットは限定的(ほとんど自分で書くのと変わりない)