Cosnomi

プログラミングをする医学生。物を作ること、自動化すること、今まで知らなかったことを知ることが好き。TypeScript書いたり、Pythonで機械学習したりなど。

Twitter / GitHub / GPG key / Fediverse / My Page
TOP >

[JavaScript] util.promisifyを少し深く調べて、限界を知った

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が全部やってくれるときの処理を追ってみましょう。何も指定していない関数に kCustomPromisifiedSymbolkCustomPromisifyArgsSymbol といった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を使うメリットは限定的(ほとんど自分で書くのと変わりない)

Comments

記事一覧へ