util.promisify()を活用しよう

こんにちは、北野です。

今回の投稿は前回の予告通り、
「util.promisify()を活用しよう」
です。
それでは行ってみましょう。

util.promisify()とは?

node v8で追加されたutilモジュールの関数の1つで、コールバックスタイルの関数を、Promiseスタイルの関数にラップします。
(コールバックスタイルの関数とは、非同期処理の結果をコールバックで受け取る形式の関数、Promiseスタイルの関数とは、非同期処理の結果をPromiseで受け取る形式の関数のことです)

例) util.promisify()の使用例 (fs.stat(path, callback)をラップする場合)

const fs = require('fs');
const util = require('util');

let statPromise = util.promisify(fs.stat);

簡単ですね。

ただし、以下の条件に当てはまる関数にしか使用できないことに注意してください。

条件1 : 関数の最後の引数がコールバック関数であること。
条件2 : コールバック関数の引数は
  第一引数 : エラー(err)
  第二引数:値(value)
であること。

上記の例として使用したfs.stat(path, callback)関数はコールバック関数が最後の引数であるため、条件1を満たしています。
また、コールバック関数はcallback(err, stats)であるため、条件2も満たしています。

逆に、setTimeout(callback, timeout)のような上記の条件を満たさない関数はutil.promisify()を適用できません。そのままではね。

カスタマイズされたutil.promisify()を使用する

util.promisify.customシンボルを使用することで、util.promisify()の動作を上書き(オーバーライド)することができます。

使用手順としては以下となります。

  1. util.promisify()が呼ばれたときにオーバーライドする処理(関数)を定義する。
  2. util.promisify()実行時に1.で定義した処理(関数)が呼ばれるように設定する。
  3. util.promisify()を使用する。

まずは以下の例を参照してください。

例) setTimeout()でutil.promisify()を使用する場合

const util = require('util');

// util.promisifyが呼ばれた時にオーバーライドする処理を定義する
function PromiseableSetTimeout(delay) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve();
    }, 
    delay);
  });
}

// util.promisifyが呼ばれたときに呼び出す関数を設定する
setTimeout[util.promisify.custom] = PromiseableSetTimeout;

// util.promisifyを使用する
let promiseSetTimeout = util.promisify(setTimeout);

上記の例では

手順1 :
util.promisify()が呼ばれたときにオーバーライドする処理(関数)を定義するは、
PromiseableSetTimeout()関数が該当します。
setTimeout()関数をPromiseでラップし、ラップしたPromiseを戻り値として返すようにしています。

手順2 :
util.promisify()実行時に手順1で定義した処理(関数)が呼ばれるように設定するは、
setTimeout[util.promisify.custom] = PromiseableSetTimeout;が該当します。
util.promisify(setTimeout)が呼ばれたときの動作をPromiseableSetTimeout()関数でオーバーライドするように設定しています。

カスタマイズされたutil.promisifyを呼び出したい関数[util.promisify.custom] =
オーバーライド処理
とすることで、util.promisify()実行時の処理をオーバーライドすることができます。

手順3:
util.promisify()を使用するは、
let promiseSetTimeout = util.promisify(setTimeout);が該当します。

以上の手順でカスタマイズされたutil.promise()を使用することができます。

※※※注意点※※※
上記の例で使用したsetTimeout()やsetImmediate()のようなよく使う関数はカスタマイズされたutil.promisify()はすでに定義されているため、本来は自分で定義する必要はありません。

(上記のsetTimeout()の例は説明のために用いたものであり、本来は公式で用意されているものを使用するようにしてください。)

公式で用意されたものがあるかはNode.jsのAPIドキュメントを参照してください。
参考としてsetTimeout()のAPIドキュメントを記載しておきます。
https://nodejs.org/api/timers.html#timers_settimeout_callback_delay_args

 

util.promisifyを使用したサンプルコード

動作確認環境

今回の動作確認環境は以下となります。

確認環境 バージョン
nodejs v8.10.0

通常版のutil.promisifyでラップした関数の成功例

コールバックスタイルの関数の処理が成功した場合の例になります。

以下の例ではまず、コールバックスタイルの関数を定義し、それをPromiseスタイルの関数にラップしています。
その後、ラップした関数を実行し、戻ってきた結果をコンソールに表示しています。
例)

const util = require('util');

// コールバックスタイルの関数
function callbackStyleFunction(data, delay, callback) {
  setTimeout(() => {
    callback(null, data);
  },
  delay);
}

// Promiseスタイルの関数にラップする
let promiseStyleFunction = util.promisify(callbackStyleFunction);

// Promiseスタイルの関数を使用する
promiseStyleFunction(10, 3000)
.then((data) => {
  // Promiseが成功した場合
  console.log('data = ' + data);
})
.catch((err) => {
  // Promiseが失敗した場合
  console.log('err' + err);
});

実行結果はプログラム開始から3秒後に

data = 10

が表示されます。

通常版のutil.promisifyでラップした関数の失敗例

コールバックスタイルの関数の処理が失敗した場合の例になります。

以下の例ではまず、コールバックスタイルの関数を定義し、それをPromiseスタイルの関数にラップしています。
その後、ラップした関数を実行し、エラー内容をコンソールに表示しています。
コールバック関数にはエラーを渡していること、その先の処理でthen()が実行されずcatch()が実行されていることを確認してください。
例)

const util = require('util');

// コールバックスタイルの関数
function callbackStyleFunction(data, delay, callback) {
  setTimeout(() => {
    let err = new Error('エラーが発生しました');
    callback(err, null);
  },
  delay);
}

// Promiseスタイルの関数にラップする
let promiseStyleFunction = util.promisify(callbackStyleFunction);

// Promiseスタイルの関数を使用する
promiseStyleFunction(10, 3000)
.then((data) => {
  // Promiseが成功した場合
  console.log('data = ' + data);
})
.catch((err) => {
  // Promiseが失敗した場合
  console.log(err);
});

実行結果はプログラム開始から3秒後に

Error: エラーが発生しました
at Timeout.setTimeout [as _onTimeout] (C:\blog\blog.js:6:15)
at ontimeout (timers.js:482:11)
at tryOnTimeout (timers.js:317:5)
at Timer.listOnTimeout (timers.js:277:5)

が表示されます。

カスタマイズ版のutil.promisifyでラップした関数の使用例

カスタマイズ版のutil.promisify()の使用例になります。

下記の例ではコールバックスタイルの関数が
第一引数:コールバック関数(引数は第一引数:データ、第二引数:エラー)
第二引数:データ
第三引数:タイムアウト時間
となっているため、util.promisify()の使用条件1, 2のいずれも満たしていません。

処理内容としては
1. util.promisify(callbackStyleFunction);が実行されたときの動作をオーバーライドする関数を定義
overridePromisify()が該当。
2. util.promisify(callbackStyleFunction);が実行されたときに動作がオーバーライドされるように設定する
callbackStyleFunction[util.promisify.custom] = overridePromisify;が該当
3. util.promisify(callbackStyleFunction);を呼び出す
としています。

例)

const util = require('util');

// コールバックスタイルの関数
function callbackStyleFunction(callback, data, delay) {
  setTimeout(() => {
    callback(data, null);
  },
  delay);
}

// util.promisify()が呼ばれたときに動作する関数を定義
function overridePromisify(data, delay) {
  return new Promise((resolve, reject) => {
    callbackStyleFunction((data, err) => {
      if (err) {
        reject(err);
        return;
      }
      resolve(data);
    },
    data,
    delay);
  });
}

// util.promisify()が呼ばれたときにoverridePromisify()が呼ばれるように設定
callbackStyleFunction[util.promisify.custom] =
  overridePromisify;

// Promiseスタイルの関数にラップする
let promiseStyleFunction = util.promisify(callbackStyleFunction);

(async function() {
  let ret = await promiseStyleFunction(10, 3000);
  console.log('ret = ' + ret);
})();

実行結果はプログラム開始から3秒後に

data = 10

が表示されます。

クラス内で定義した関数にutil.promisify()を使用する場合の使用例

クラス内で定義した関数にutil.promisify()を使用した例となります。

クラス内で定義した関数にthisを使用していない場合は上記までの方法で問題ありませんが、thisを使用している場合はそのまま使用するとエラーが発生します。

例)

const util = require('util');

class TestClass {
  constructor() {
    this.name = 'cat';
  }

  // コールバックスタイルの関数
  callbackStyleFunction(data, callback) {
    console.log('name = ' + this.name + ', data = ' + data);
    callback(null, data);
  }
};

// クラスをインスタンス化
let testClass = new TestClass();

// Promiseスタイルの関数にラップする
let promiseStyleFunction = util.promisify(testClass.callbackStyleFunction);

(async function() {
  let ret = await promiseStyleFunction(10);
  console.log('ret = ' + ret);
})();

実行結果は以下のようになります。

TypeError: Cannot read property ‘name’ of undefined
at callbackStyleFunction (C:\blog\main.js:14:34)
at callbackStyleFunction (internal/util.js:230:26)
at C:\blog\main.js:24:19
at Object.<anonymous> (C:\blog\main.js:26:3)
at Module._compile (module.js:652:30)
at Object.Module._extensions..js (module.js:663:10)
at Module.load (module.js:565:32)
at tryModuleLoad (module.js:505:12)
at Function.Module._load (module.js:497:3)
at Function.Module.runMain (module.js:693:10)

この呼び方ではクラスの関数内で使用しているthisがundefinedになるためname変数が見つからず発生しています。

これを回避するためのは上記のソースを次のようにしてthisオブジェクトを事前にバインドします。

testClass.callbackStyleFunction

↓クラスの関数.bind(インスタンス変数)の形に変更

testClass.callbackStyleFunction.bind(testClass)

bind()関数についてはいくつかの使い方があるのですが、今回はthisを固定化(バインド)するために使用しています。

今回の場合、bind()関数の戻り値はthisが固定化された関数オブジェクトとなります。

修正版のサンプルは以下のようになります。

例)

const util = require('util');

class TestClass {
  constructor() {
    this.name = 'cat';
  }

  // コールバックスタイルの関数
  callbackStyleFunction(data, callback) {
    console.log('name = ' + this.name + ', data = ' + data);
    callback(null, data);
  }
};

// クラスをインスタンス化
let testClass = new TestClass();

// Promiseスタイルの関数にラップする
let promiseStyleFunction = util.promisify(testClass.callbackStyleFunction.bind(testClass));

(async function() {
  let ret = await promiseStyleFunction(10);
  console.log('ret = ' + ret);
})();

実行結果は

name = cat, data = 10
ret = 10

となり、意図通りの出力となりました。

外部のライブラリ等を使用する場合、このケースに当てはまる場合もありますので、変数が見つからないの等のエラーになった場合は一度bind()関数でthisの固定化を行ってみると解決するかもしれません。

今回の記事は以上となります。
お疲れさまでした。

次回は今回の内容と対となるutil.callbackify()を予定しています。

それではまた、次の記事にて!

コメント

タイトルとURLをコピーしました