JavaScript
Node.js
promise
More than 1 year has passed since last update.

この記事は、さくらインターネット Advent Calendar 2015 2日目の記事です。

コールバック

JavaScriptでコーディングしていると、コールバックの多さに頭を抱える時があると思います。JSとブラウザの下のような性質によるものです。

  • JSは基本的にシングルスレッド
  • JSの実行中にUIを更新することが出来ない
  • ユーザの操作もキューに格納され、処理が完了し次第順次実行される

なので、ブロック処理を書くことは難しく、コールバックを使用して非同期処理を実現しています。

コールバックを使用したコードの例として書いてみました。

// ./configファイルを読み取り、その中に書かれた言語の翻訳ファイルを取得する
// Node.jsを使用した例
var fs = require("fs");
fs.readFile("./config", function(err, configBuff){
  if(err)
    console.warn("エラー", err);
  var configJson = JSON.parse(configBuff.toString());
  fs.readFile("./lang/" + configJson.lang, function(err, langBuffer){
    if(err)
      console.warn("エラー", err);
    var langJson = JSON.parse(langBuffer.toString());
    console.log("正常終了", langJson);
  });
});

このコードの短所をまとめました。

  • ネストが深くかる
  • 同じエラー処理を2回書いている
  • buffer.toStreing()とJSON.parse()の処理を2書いている

やろうとすることが多くなるごとにコードがどんどん複雑になるという問題を抱えています。このコードを綺麗にすることを目的にPromiseを使ってみましょう。

Promise

一口にPromiseと言っても色々有ります。みんながPromisePromiseと言っている物は、Promise/A+という仕様に準拠したものになっています。

https://promisesaplus.com/

そして、これに準拠した主なものは以下のとおりです。

これらについて、少し触れていこうと思います。

JavaScriptのビルドインオブジェクトPromise

Promise - JavaScript | MDN
Promise - Can I use...

いわゆる標準化された有名なPromiseです。ブラウザでも一部使用できるようになっています。これで、fs.readFileをPromise化してみましょう。

var fs = require("fs");
var promiseReadFile = function(filename){
  return new Promise(function(resolve, reject){
    fs.readFile(filename, function(err,data){
      if(err) reject(err);
      else resolve(data);
    });
  })
};

Promiseインスタンスを作成するときに引数としてresolve,rejectを受け取るコールバックを渡します。正常終了した場合は、引数として渡されたresolve()にデータを渡します。エラーの場合は、reject()にエラーを渡します。どちらの場合も、データを渡すかは自由なので必要が無ければ指定しなくて結構です。

使用方法1 - 1つの処理

使用方法はこんな感じです。ここまでなら、書き方が変わっただけで喜びは少ないと思います。

promiseReadFile("config")
  .then(function(configBuff){
    console.log("正常終了", configBuff.toString());
  })
  .catch(function(error){
    console.warn("エラー", error);
  });
  • Promiseに対して.then()を使用することによって正常終了した場合の処理を記述すことが可能
    • .then()の中には、引数でデータを受け取り次のthenに渡す値を戻り値として返す関数を記述
    • .then()は複数繋げることが出来る
  • .catch()を使用することによってエラーが発生した際の処理を記述することが可能
    • .catch()reject()が呼ばれた時は勿論の事、上の例のpromiseReadFile()then()内で起こった例外を掴むことが可能

使用方法2 - 複数の処理

Promiseの本領が発揮するのは、複数の処理をつなげる時だと思います。

var paseJson = function(buffer){
  return JSON.parse(buffer.toString());
};
promiseReadFile("config")
  .then(function(configBuff){
    var configJson = paseJson(configBuff);
    return promiseReadFile("./lang/" + configJson.lang);
  })
  .then(function(langBuffer){
    var langJson = paseJson(langBuffer);
    console.log("正常終了", langJson);
  })
  .catch(function(error){
    console.warn("エラー", error);
  });

複数処理のエラーハンドリングも行え、複数の処理をネストしていないように書くことが出来きました。お気付きだとは思いますが、then()でPromiseを返すとその処理に関しても待ってくれるようになります。then()には通常の値かPromiseを渡すことが出来ます。

使用方法3 - thenでフィルタ

then()で次のthen()に渡すためのデータを通常の値で返せるという性質を用いると次のような事もできます。

var paseJson = function(buffer){
  return JSON.parse(buffer.toString());
};
promiseReadFile("config")
  .then(paseJson)
  .then(function(configJson){
    return promiseReadFile("./lang/" + configJson.lang);
  })
  .then(paseJson)
  .then(function(langJson){
    console.log("正常終了", langJson);
  })
  .catch(function(error){
    console.warn("エラー", error);
  });

このように、フィルタのような処理を簡単に書くことが出来ました。

以上でthen()でPromiseや値を返せば簡単に非同期の処理がチェーンが書けるようになることがお分かりいただけたでしょうか。他にも、配列でPromiseを渡すと全て完了するまで待ってくれるPromise.allなどもあるので、興味があればMDNなどを御覧ください。

Q

Q - npm

こちらは、Qというnode.jsのモジュールです。導入方法はコマンドでnpm install qと叩くだけです。

基本的な使用方法は上のPromiseと同じです。機能を更に増強したバージョンです。主な機能を挙げます。

  • 多彩な生成方法
  • タイムアウト値の設定
  • NodeJsのようなコールバックメソッドを簡単にPromise化
  • ディレイを簡単に設ける
  • 複数のPromiseを扱う際の強力な機能
  • 処理中の進捗を報告するNotify/Progress機能

QでPromiseの生成

Qには多彩な生成方法があるので、使いやすいものを選びましょう。
それぞれすべてQのPromiseを返します。

同期処理

通常の関数を呼び出し、QのPromiseへと変換します。

var Q = require("q");

Q.fcall(function() {
  // 処理…
  if(result) return result;  // 正常
  throw new Error("エラー");  // 異常
}).then(function(data){

}).catch(function(error){

}).done();

値かPromiseを渡してQのPromiseを作成します。JS標準でのPromiseからの変換などにも使えます。

Q.when("テキスト");

コールバック処理

Q.Promiseを用いて、JS標準のPromise生成のようなフローで生成します。

var qReadFile = function(filename){
  return Q.Promise(function(resolve, reject, notify){
    fs.readFile(filename, function(error, buffer) {
      if (error) reject(error);
      else resolve(buffer);
    });
  }
};

qReadFile("testfile.txt")
  .then(function(buffer){
  })
  .catch(function(error){
  }).done();

上と同じようなフローですが、Q.deferのrejectとresolveを使用します。

var qReadFile = function(filename){
  var deferred = Q.defer();
  fs.readFile(filename, function(error, buffer) {
    if (error) deferred.reject(new Error(error));
    else deferred.resolve(buffer);
  });
  return deferred.promise;
};

Node.js等の関数の呼び出しと変換

callbackが(error, data)である関数は以下の様な呼び出しが可能です。

return Q.nfcall(fs.readFile, "text.txt", "utf-8");
return Q.nfapply(fs.readFile, ["text.txt", "utf-8"]);
return Q.ninvoke(fs, "readFile", "text.txt", "utf-8");
return Q.npost(fs, "readFile", ["text.txt", "utf-8"]);

Promiseな関数に手軽に変換する方法です。

var readFile = Q.denodeify(fs.readFile);
return readFile("text.txt", "utf-8");

var readFile = Q.nbind(fs.readFile, fs);  // バインドして呼び出すことが可能
return readFile("text.txt", "utf-8");

上に出てきたDeferを使用して変換する方法です。

var readFile = function(filename){
  var deferred = Q.defer();
  fs.readFile(filename, "utf-8", deferred.makeNodeResolver());
  return deferred.promise;
};

Qの便利な機能

処理に対してタイムアウトを設定することが出来ます。
また、処理中の進捗をnotifyとprogressで報告することが出来ます。

// ランダムで処理時間が決まる
// 100ms毎に通知する
var randomTime = function(){
  return Q.Promise(function(resolve, reject, notify) {
    var fn = function(){
    setTimeout(function() {
      notify("Progress...");
      if(Math.random() > 0.1)
        fn();
      else resolve("OK");
    }, 100);
    };
    fn();
  });
}

//1000ms以上かかるとでNGになる
randomTime()
  .timeout(1000)
  .progress(function(data){
    console.warn(data);
  })
  .then(function(){
    console.log("OK!");
  })
  .catch(function(){
    console.warn("NG...");
  }).done();

このような結果になります。

>node index.js
Progress...
Progress...
Progress...
Progress...
OK!

>node index.js
Progress...
Progress...
Progress...
Progress...
Progress...
Progress...
Progress...
Progress...
Progress...
NG...

複数の処理を同時に行うことも出来ます。その際に引数の受け取りに便利なメソッドも用意されています。

Q.all([  // すべての処理が終わるまで待機
  Q.when(1),
  Q.when(2)
]).spread(function(res1, res2){  // thenなら配列で渡される結果を、順番に引数に展開
  console.log(res1, res2);  // "1 2"と出力
});

Qでの注意点

Qではエラーハンドリングを明示的に書かなければ外にエラーが伝達しない可能性があります。何度か登場していますが、.done()を最後につけることによって.catch等を設定していない時にエラーが握りつぶされることが無くなります。これを無くす機能を現在検討している模様です。

他の機能などに関しては、Wikiに詳しく乗っています。
https://github.com/kriskowal/q/wiki

Bluebird

bluebird - npm
Getting Started | bluebird

最後になりましたが、魔法を少し紹介します。(あまり使ったことが無いのでちょっとだけ)
Bluebirdは他には無い便利な機能を持っています。
bluebird.promisifyAllを実行することによって、パッケージ内のすべてのメソッドにPromise版を追加する事ができます。

var Promise = require("bluebird");
var fs = Promise.promisifyAll(require("fs"));

fs.readFileAsync("c:\/test.txt")  // 通常のメソッド名にAsyncを足した名前になる
  .then(function(data){
    console.log(data);
  });

他にもQに劣らない多彩なAPIが用意されているので、興味が有る方はドキュメントを御覧ください。
http://bluebirdjs.com/docs/api-reference.html

まとめ

Promiseを使うことによって、見通しの良いコーディングができるようになると思います。
ES6で追加された、アロー演算子との相性も良いと思いますので積極的に使って行きたいです。