pig's diary

何でも忘れるので万年初心者ね

iTerm, Vim, Solarized, command-t

MacVim.app と Terminal.app を交互に切り替えて使っていたけど、iTerm を使うと全画面でターミナル、Vim を表示できるらしい。

  1. iTerm2 をインストール
  2. さて Vim を起動
    • :colorscheme solarized
    • 色がなんか変。iterm 側に色のプリセットを入れる必要があるらしい。
  3. iterm2-colors-solarized
  4. さて Vim
    • そういえば command-t が使えない(MacVim.app では動いていた、という前提です)
    • mac デフォルトの /usr/bin/vim は --enable-rubyinterp (ruby support) でコンパイルされてない。
    • $ brew install vim -f
      • brewmac のコマンドを大切に思っているので --force オプションを付けないと Vim をインストールできない
      • brew で入る Vim には ruby surrpot が付いている。他には必要ないのでデフォルトのままで行く。
    • /Applications/MacVim.app を消しておく
    • 欲しい Vim が入った。
  5. $ which vim
    • /usr/bin/vim 。古いまんまだ
    • $PATH の順番を変えたい
    • $ sudo vim /etc/paths
      • /usr/local/bin を上に持って来て保存
    • シェルを立ち上げ直せば /usr/local/bin/vim を先に見られるようになってる


Command-d で縦分割、Command-] でペイン移動できるようになった。"mouseless copy" とか便利そう。


参考:

マージャンのやつ JavaScript

あなたのスキルで飯は食えるか? 史上最大のコーディングスキル判定
http://www.itmedia.co.jp/enterprise/articles/1004/03/news002_2.html

※トータルで1週間以上かかっているorz

var assert = require('assert');



var test = function(results, answers) {
    if (results == null && answers == null) return;
    assert.equal(results.length, answers.length);
    answers.forEach(function(answer) {
        assert(~results.indexOf(answer));
    });
};



var machi = (function() {
        
    var format = function(answer) {
        var s = '';
        answer.grouped.sort().forEach(function(g) {
            s += '(' + g + ')';
        });
        return s + (answer.rest ? '[' + answer.rest + ']' : '');
    };

    var collectFlattens = function(input, enough) {
        var regexp = new RegExp('(.)\\1{' + (enough - 1) +'}', 'g');
        return input.match(regexp);
    };
    test(collectFlattens('112224588899', 2), ['11', '22', '88', '99']);
    test(collectFlattens('12345', 2), null);
    test(collectFlattens('1112224588899', 3), ['111', '222', '888']);
    test(collectFlattens('124589', 3), null);

    var collectAscendings = function(input, enough) {
        var results = [];
        var collected;
        var curr;
        var foundIndex;
        var isEnough = function() {
            return collected && collected.length >= enough;
        };
        while (input.length >= 2) {
            collected = '';
            curr = 0;
            foundIndex = -1;
            while (~(foundIndex = input.indexOf(+input[curr] + 1, curr + 1))) {
                if (!collected) collected = input[curr];
                collected += input[foundIndex];
                if (isEnough()) break;
                curr = foundIndex;
            }
            if (isEnough()) results.push(collected);
            input = input.slice(1);
        }
        return results.length >= 1 ? results : null;
    };
    test(collectAscendings('1122335556799', 3), ['123', '123', '567', '567', '567']);
    test(collectAscendings('112255578', 3), null);

    var getRest = function(collected, original) {
        collected.split('').forEach(function(letter) {
            original = original.replace(letter, '');
        });
        return original;
    };
    assert.equal(getRest('123', '1122335556799'), '1235556799');
    assert.equal(getRest('567', '11225556799'), '11225599');
    assert.equal(getRest('789', '1122555789'), '1122555');

    var hasHead = function(grouped) {
        return grouped.some(function(group) {
            return group.length == 2;
        });
    };
    assert.equal(hasHead(['234', '555', '888']), false);
    assert.equal(hasHead(['234', '555', '88']), true);



    return function(original) {
        var answers = [];
        (function collect(grouped, input) {

            var finalizer = function(candidates) {
                if (!candidates) return;
                candidates.forEach(function(newGroup) {
                    var newGrouped = grouped.slice(0);
                    newGrouped.push(newGroup);
                    var rest = getRest(newGroup, input);
                    if (rest && (
                            collectFlattens(rest, 3) ||
                            collectAscendings(rest, 3))) {
                        collect(newGrouped.slice(0), rest);
                    } else if (rest.length <= 3) {
                        var heads = collectFlattens(rest, 2);
                        if (rest.length == 1 ||
                                (heads && heads.length >= 1) ||
                                collectAscendings(rest, 2)) {
                            var a = format({
                                grouped: newGrouped,
                                rest: rest
                            });
                            if (!~answers.indexOf(a)) answers.push(a);
                        }
                    }
                });
            };

            if (!hasHead(grouped)) {
                finalizer(collectFlattens(input, 2));
            }
            finalizer(collectFlattens(input, 3));
            finalizer(collectAscendings(input, 3));

        })([], original);
        return answers;
    };

})();

test(machi('1112224588899'), [
    '(111)(222)(888)(99)[45]'
]);
test(machi('1122335556799'), [
    '(123)(123)(55)(567)[99]',
    '(123)(123)(555)(99)[67]',
    '(123)(123)(567)(99)[55]'
]);
test(machi('1112223335559'), [
    '(123)(123)(123)(555)[9]',
    '(111)(222)(333)(555)[9]'
]);
test(machi('1223344888999'), [
    '(123)(44)(888)(999)[23]',
    '(123)(234)(888)(999)[4]',
    '(234)(234)(888)(999)[1]'
]);
test(machi('1112345678999'), [
    '(11)(123)(456)(999)[78]',
    '(11)(123)(678)(999)[45]',
    '(11)(345)(678)(999)[12]',
    '(11)(123)(456)(789)[99]',
    '(111)(234)(567)(99)[89]',
    '(111)(234)(789)(99)[56]',
    '(111)(456)(789)(99)[23]',
    '(123)(456)(789)(99)[11]',
    '(111)(234)(567)(999)[8]',
    '(111)(234)(678)(999)[5]',
    '(111)(345)(678)(999)[2]'
]);

Ti ヘッダ 取りにくい

検索クエリ「Ti ヘッダ 取りにくい」の方にのみお送りしております。

私の場合 Titanium でHTTP 通信したくなった結果こういうことになりました。

気づいたことなど

  • iPhone だと、getAllResponseHeaders できないらしい。(動きとドキュメント見た限り)だから、ヘッダは個別に取得する必要がある。
  • iPhone シミュレータで、 req.getStatus() するとどうしてもエラーになる。req.status すると素直に取れた。ドキュメントの嘘つき・・。
  • Titanium Studio についてる Ajax Monitor は、ブラウザの通信しか教えてくれない。Ti.Network.createHTTPClient でやるなら、WireShark とか使う必要がありあそう。
  • いろいろとたまに動かなくなって、JavaScript がミスってるのか、不可解なTitanium のエラーなのかが分からなくなってきてしんどい。慣れてきたけど。
  • 非同期管理モジュール q を使ってみた。node 系のモジュールがそのまま使えるからありがたい。
  • 追記)cookie は勝手に管理してくれているようだ。
  • 追記)Titanium Studio を開くたびに「アップデートがあるよ」と言われる。
// 使用例

var http = require('http');

// リクエストを送る
http.post('http://localhost:3000/post', {
    title: 'タイトル',
    body: '本文'
})
.then(function(res) {
    console.log(res.getStatus()); // 200 とか
    console.log(res.getJson()); // responseText の JSON.parse 結果
    console.log(res.getHeader('Content-Type')); // 'application/json;charset=UTF8'
})
.fail(function(reason) {
    console.log(reason); // 失敗した理由
});

http.js の中身

var Q = require('../libs/q');
var Response = require('./response');
var _ = require('../libs/underscore');
var URL = require('../libs/url');
var querystring = require('../libs/querystring');



/**
 * @param {string} url .
 * @param {Object} params .
 */
var buildUrlForGet = function(url, params) {
  url = URL.parse(url);
  url.query = _.extend(url.query, querystring.parse(params));
  return url.toString();
};



/**
 * @param {string} method .
 * @param {string} url .
 * @param {Object} params .
 * @return {Object} Promise to response.
 */
var request = function(method, url, params) {
  var defer = Q.defer(),
      usePost = method === 'POST',
      req = Ti.Network.createHTTPClient();

  if (!usePost && params) {
    url = buildUrlForGet(url, params);
  }

  if (usePost) {
    req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  }
  req.setAutoRedirect(false);

  req.onload = function() {
    defer.resolve(new Response(req));
  };
  req.onerror = function(reason) {
    defer.reject({ reason: reason });
  };

  req.open(method, url);
  req.send(usePost && params);

  return defer.promise;
};



/**
 * @param {string} url .
 * @param {Object} query .
 * @return {Object} Promise to response.
 */
var get = function(url, query) {
  return request('GET', url, query);
};

/**
 * @param {string} url .
 * @param {Object} body .
 * @return {Object} Promise to response.
 */
var post = function(url, body) {
  return request('POST', url, body);
};

module.exports.get = get;
module.exports.post = post;

更にクッキーを保存・送信する仕組みも必要だなー。大変だ。

追記)クッキーは自動的に保存しているようだった。2度目に作った HTTPClient には、1度目の responseHeader の Set-Cookie がちゃんと載っている。

WebDriverJs 〜果たされた約束〜

相変わらずSEO無視気味のうざいブログタイトルシリーズです。

WebDriverJs というのがあって、SeleniumJavaScriptドライバです。npm で簡単に入れられます。

$ npm install selenium-webdriver

https://code.google.com/p/selenium/wiki/WebDriverJs

WebDriverJsは私の中で話題のPromiseを全面的にサポートしています。

それで、SeleniumはPromiseと非常に相性がいいと思うんです。

Seleniumはブラウザ自体の操作や状態を知るのは得意ですが、スクリプトで変化したページの状態を知るのは苦手な部分があります。例えばこれは得意です:ページ遷移後、ページが表示されたタイミングに、ページタイトルを確認してテストする。でも、私たちが実際にテストしたいのは、もっとこういうものです:ページ内のボタンをクリックしたら、XHR通信が発生し、サーバの健康状態にもよるが、2秒後ぐらいにレスポンスが返り、ページ内の複数のDOMが書き換えられるので、その一連の動作をテストしたい。

XHR後でかつレンダリングが終わったタイミングというのは、確かに知りにくいです。XHR通信の到着直後にテストをしてもいけない。レンダリングが終わった直後にテストをしてもいけない。他でもなく、テストしたい場所がテストできる状態になってからテストしたいのです。これは、ブラウザAPIでは検出しにくい情報です。もっと緩やかな(人間の視点に近い)条件でステップを進行できないものかな? と私はかれこれ中学生くらいの時からずっと考えていました。嘘です。

それで、方法がありました。Promiseを用いたSeleniumテストです。PromiseといえばJavaScriptです。JavaScriptは私が唯一書ける言語です。ラッキーです。

Promiseは、非同期テストに最適です。Promiseは処理の塊をステップとして扱い、それらを勝手に順序よく実行してくれます。ステップを外に出し、複数のテストでステップを共有することもできます。ステップをネストさせれば(たぶんWebDriverJsでもできると思うんだが)、よくある一連の操作を1つにまとめて使うこともできます。テストがスケールするかどうかは、それが増えて来た時に必ず重要になるでしょう。Promiseなら、理解し易い形で処理を増やして行くことができるでしょう。

例えば、こういう変なページがあったとしましょう。今回これをテストします。

タイトルは無視してください。作り直すのが面倒です。この画面をしばらく眺め、pushボタンをクリックしたくなったら(貴方はきっとなる)クリックします。すると2秒後に・・・

successと表示されます。わざと2秒もかかるようにしてみました。サーバのコードはこちらです。

// Node server
app.get('/timeout', function(req, res) {
  setTimeout(function() {
    res.end(JSON.stringify({
      success: 1
    }));
  }, 2000);
});

1. クリックする、2. success と表示される。それでは、この動作を実際にテストをしてみます。mochaしか使えませんのでmochaを使います。CoffeeScriptが書けますのでCoffeeScriptで書きます。

# CoffeeScript



assert = require("assert")
webdriver = require("selenium-webdriver")
buildDriver = ->
    d = new webdriver.Builder().usingServer("http://localhost:4444/wd/hub").withCapabilities(browserName: "firefox").build()
    d.manage().window().setSize 350, 200
    d



describe "strange page", ->

    it "should display 'success' on click", (done) ->

        # DOMセレクタたち。
        BUTTON = webdriver.By.id("b")
        BOX = webdriver.By.id("box")

        # :3000にアクセスする。
        d = buildDriver()
        d.get "http://localhost:3000/"

        # クリックして、"success" と表示されるまで最大4秒まで待つ。
        d.findElement(BUTTON).click()
        d.wait (->
            # この条件式が何度も実行される
            d.findElement(BOX).getText().then (text) -> text is "success"
        ), 4000, "OMG"

        # 一応テストしてみる。
        d.findElement(BOX).getText()
        .then((text) -> assert.equal text, "success") # 100%通るはず。

        # どこかでエラーが起きたら、ここに来る。
        .then(null, (reason) ->
            d.quit().then -> throw new Error(reason)
        )

        # 問題なければテストを終了。
        .then ->
            d.quit().then -> done()

WebDriverJs は.click()や.findElement() などの関数をコールすると、Promiseオブジェクトを返します。でもthenしていないのは、コールするだけで、Promiseがスケジュールに自動的に追加されて行くからです。つまり、d.then(function(){ return d.findElement(BOX).getText() }).then(...) とも書けますが、代わりに上記のようにも淡々とスケジュールを登録していくことができます。これは npm q には無い考え方でした。

重要なのは driver.wait(condition, ms, message) です。第1引数に、Booleanを返すFunctionを渡しましょう。すると、何度も何度もこの関数が呼ばれ、trueになるまで、ステップを待機する事ができます。つまり、任意の条件を作り出し、ページがその状態になるまでステップを止めることができます。今回の2秒かかるレスポンスのテストでは、condition関数が13回呼ばれていました。

会社の人が言っていましたが、この「何度も叩く」仕組みはテストフレームワークのJasmineの非同期テストと同じ仕組みのようです。合理的です。mochaには恐らく無く、mochaでは非同期後にユーザーが done() を実行することでテストが完了する仕組みを採用しています。それで十分だし、Promiseのresolveの考え方が好きなので私は気に入っています。

$ mocha

  -----------------------------
  &#8901;&#8901;&#8901;&#8901;&#8901;&#8901;&#8901;&#8901;&#8901;&#8901;&#8901;&#8901;&#8901;&#8901;&#8901;&#8901;&#8901;&#8901;&#8901;&#8901;&#8901;&#8901;&#8901;&#8901;&#8901;&#8901;&#8901;&#8901;&#8901;&#9992;
  -----------------------------

  1 test complete (6 seconds)

$

ビィィュュュォォァァァ・・・・。着陸成功です。ブラウザを立ち上げたりするのに時間がかかりましたが、Promiseのおかげで最短に近いスピードだと思います。

あとは特に書くこともないですが、mochaのデフォルトタイムアウトは2000ミリ秒なので mocha.opts に --timeout 10000 などと書いておきましょう。

PromiseとDeferredを表面的に実装してみた。

promiseにはpromise/A という小さなルールがあって、それに則って皆ライブラリを作っているみたいだけど、それはいいからとにかくthen したくなった。

主に、promiseオブジェクトのありかたが本家を無視しています。promiseオブジェクトは、thenを一度しか呼べないルールなんじゃないか。で、thenしたら 新たなpromiseオブジェクトをnew してreturn するんじゃないかと思ってる(全部詳細未確認)。

TODO: あとでちゃんと kriskowal/q のソースよむ。

var util = require('util');


/**
 * @constructor
 */
var Promise = function(startValue) {
  this.startValue = startValue;
  this.chain = [];
};

// thenすると、ステップを蓄積していく。
Promise.prototype.then = function(f) {
  this.chain.push(f);
  return this;
};

// doneすると、最後のステップを挿入したあと、実行を開始する。
Promise.prototype.done = function(f) {
  this.then(f).call_(this.startValue);
};

/**
 * @protected
 */
Promise.prototype.call_ = function(val) {
  var me = this;

  // nextTickから実行開始。
  process.nextTick(function() {
    var rv = me.chain.shift()(val);
    if (me.chain.length != 0) {
      // 蓄積されたステップを、nextTick ごとにcallしていく。
      me.call_(rv);
    }
  });
};

(new Promise).then(function() {
  return 'a';
}).then(function(a) {
  return a + 'b';
}).then(function(b) {
  return b + 'c';
}).done(function(out) {
  console.log(out); // 'abc'
});



/**
 * @constructor
 * @extends {Promise}
 */
var Deferred = function() {
  Promise.call(this);
};
util.inherits(Deferred, Promise);

/**
 * @protected
 */
Deferred.prototype.call_ = function() {
  var me = this;
  var args = Array.prototype.slice.call(arguments);

  // 次の関数を呼ぶための準備をする。doneなら無視。
  if (me.chain.length >= 2) {
    args.unshift(function next() {
      if (me.chain.length > 0) {

        // next が呼ばれたら、次のステップに移れるように。
        me.call_.apply(me, arguments);
      }
    });
  }

  // nextTickから実行開始。
  process.nextTick(function() {
    // 蓄積した最初の関数を実行。
    me.chain.shift().apply(null, args);
  });
};


(new Deferred).then(function(next) {
  setTimeout(function() {
    next('a');
  }, 800);
}).then(function(next, a) {
  util.print(a);
  setTimeout(function() {
    next('--', 'b');
  }, 800);
}).then(function(next, and, b) {
  util.print(and + b);
  setTimeout(function() {
    next('c');
  }, 800);
}).done(function(c) {
  console.log(c);
});
// 'a', '--b', 'c' と非同期的に出力される

苛烈!マングースは3度噛む

node.js のMongoDBドライバに Mongoose というものがあります。これの model.update 関数のコールバックの動きが分かりにくかったので、注意して様子を見てみました。

まず、ネイティブのREPLの動きを再確認します。

$ mongo
MongoDB shell version: 2.2.1
connecting to: test

// 新規データベースを作成
> use myBalanceBook
switched to db myBalanceBook

// 文書の作成
> db.girls.insert({name: 'honda tsubasa', spent: 500})  // 1つめの文書を作成
> db.girls.insert({name: 'mizuhara kiko', spent: 1500})  // 2つめ
> db.girls.insert({name: 'nounen rena', spent: 20})  // 3つめ

// 文書の書き換え
> db.girls.update({name: 'nounen rena'}, {xxx: 'xxx'})  // 選択した1つの文書を上書き
> db.girls.find()
{ "_id" : ObjectId("516a84a543b0013848d9a9ac"), "name" : "honda tsubasa", "spent" : 500 }
{ "_id" : ObjectId("516a84ce43b0013848d9a9ad"), "name" : "mizuhara kiko", "spent" : 1500 }
{ "_id" : ObjectId("516a84f543b0013848d9a9ae"), "xxx" : "xxx" }  // 上書かれてしまった文書

// 文書にキーを追加
> db.girls.update({name: 'mizuhara kiko'}, {$set: {age: 22}})  // 選択した文書に、ageキーを追加
> db.girls.find()
{ "_id" : ObjectId("516a84a543b0013848d9a9ac"), "name" : "honda tsubasa", "spent" : 500 }
{ "_id" : ObjectId("516a84f543b0013848d9a9ae"), "xxx" : "xxx" }
{ "_id" : ObjectId("516a84ce43b0013848d9a9ad"), "age" : 22, "name" : "mizuhara kiko", "spent" : 1500 }  // ageキーが追加された文書

// upsert(あればupdate、無ければinsert)
> db.girls.update({name: 'kojima fujiko'}, {name: 'kojima fujiko', spent: 4}, true)  // upsert(第3引数がtrue)
> db.girls.find()
{ "_id" : ObjectId("516a84a543b0013848d9a9ac"), "name" : "honda tsubasa", "spent" : 500 }
{ "_id" : ObjectId("516a84f543b0013848d9a9ae"), "xxx" : "xxx" }
{ "_id" : ObjectId("516a84ce43b0013848d9a9ad"), "age" : 22, "name" : "mizuhara kiko", "spent" : 1500 }
{ "_id" : ObjectId("516a87be9a5e6b80d7b75284"), "name" : "kojima fujiko", "spent" : 4 }  // upsert(insert)された文書

// multi update (複数の文書を書き換え)
> db.girls.update({}, {$inc: {spent: 1000}}, false, true)  // multi(第4引数がtrue):セレクタにヒットする全ての文書に対し、変更を加える
                                                                                    // (ここでは、spentキーの値を1000インクリメントする)
> db.girls.find()  // 全ての文書のspentキーの値が1000増えている
{ "_id" : ObjectId("516a84a543b0013848d9a9ac"), "name" : "honda tsubasa", "spent" : 1500 }
{ "_id" : ObjectId("516a84f543b0013848d9a9ae"), "spent" : 1000, "xxx" : "xxx" }
{ "_id" : ObjectId("516a84ce43b0013848d9a9ad"), "age" : 22, "name" : "mizuhara kiko", "spent" : 2500 }
{ "_id" : ObjectId("516a87be9a5e6b80d7b75284"), "name" : "kojima fujiko", "spent" : 1004 }
> 

注意すべき点はここです。

  • updateは、デフォルトでは1件のみを書き換える
  • multiフラグ(第4引数)がtrueの場合、セレクタ(第1引数)にヒットした全ての文書を書き換える

それでは、NodeからMongoDBを操作するドライバmongoose の update関数の動きを見て行きます。

ドキュメント: http://mongoosejs.com/docs/api.html#model_Model.update

予め、上記データベースのgirlsコレクションを操作するためのGirlModelを作成しておきます。

var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/myBalanceBook');
var GirlModel = mongoose.model('girls', {
  name: String,
  spent: Number,
  cantAfford: Boolean,
  age: Number
})

それでは文書1件に対し、spentキーの値をインクリメントします。

GirlModel.update({name: 'mizuhara kiko'}, {$inc: {spent: 2000}}, function(err, numberAffected, raw) {
  console.log(err); // null
  console.log(numberAffected); // 1
  console.log(raw);
  // {
  //   updatedExisting: true,
  //   n: 1,
  //   connectionId: 48,
  //   err: null,
  //   ok: 1
  // }  
});

1件の文書を操作しました。コールバック関数の引数には何が渡って来るのでしょうか?
第1引数からは、通常のNodeのコールバックと同じように、エラーが起きた場合エラーオブジェクトが来ます。
第2引数は、操作した文書の件数です。通常は0 か 1 が来るはずです。後述するmultiフラグをtrueにしない限り、書き換えは最大1件に対してしか行わないからです。
第3引数は、rawとありますから、生データのようです。このうち、

  • updatedExisting: 何か1つでもセレクタにヒットしたらtrue
  • n: 操作された文書の件数(コールバックの第2引数numberAffectedと同じ)
  • connectionId: 知りません。どうせリクエストのUIDか何かでしょう。
  • err: rawで返されるエラーオブジェクトのようです。
  • ok: オッケーなら1、ダメなら0 のようです。試しにnumberAffected がゼロの結果でも、 ok は 1 でした。errと一体何が違うんだ。

続いて、mongooseから複数の文書をupdateしてみます。

GirlModel.update({
  spent: { $gt: 50 }
}, {
  $set: {cantAfford: true}
}, {
  multi: true
}, function(err, numberAffected, raw) {
  console.log(err); // null
  console.log(numberAffected); // 4
  console.log(raw);
  // {
  //   updatedExisting: true,
  //   n: 4,
  //   connectionId: 69,
  //   err: null,
  //   ok: 1
  // }  
});

mongooseでは、multiなどのオプションを第3引数にオブジェクトでまとめて渡します。そのためコールバック関数は第4引数になります。multiフラグをtrueにしたので、操作された文書の件数:numberAffected が 4 になっているのが分かります。ふむふむ。

書いていて気づいたのですが、言うほど分かりにくくはなかったです。

npm q べんり

qはPromises/A提案ベースの非同期管理ユーティリティです。
https://github.com/kriskowal/q/wiki/API-Reference
と思ってたら、今はPromises/A+ というものがあるようです。
http://promises-aplus.github.io/promises-spec/

qがあれば、非同期処理が増えてインデントが増えて読みにくくなったり、クロージャ構造で変数を参照し過ぎてリファクタリングしにくいコードになるのを、ある程度防ぐことができます。

以下は、同期処理と非同期処理を順番に処理していく例です。

var Q = require('q');

var step1 = function(v) {
    var d = Q.defer();
    setTimeout(function() {
        d.resolve(v);
    }, 100);
    return d.promise;
};

var step2 = function(v) {
    console.log(v);
};

var step3 = function() {
    var d = Q.defer();
    setTimeout(function() {
        d.resolve('ohh');
    }, 800);
    return d.promise;
};

// 同期的に 'yeah' という文字列を取得するところからスタート
Q.when('yeah')

// 非同期処理。引数に 'yeah' が渡ってくる。ここでは、100ms 後に、もういちど文字列 'yeah' を返す。
.then(step1)

// 同期処理。前のステップが終わってから実行される処理。ここでは、渡ってきた値を出力する。
.then(step2)

// 再び非同期処理。今度は、800ms後に文字列 'ohh' を渡す。
.then(step3)

// お終い。最後のステップで返される文字列 'ohh' が渡ってくる。
.done(function(v) {
    console.log(v, '... done. ');
});

上の例では非同期処理がただのsetTimeoutですが、これが「ファイルアクセス」や「HTTPリクエスト」等の非同期だったとして考えてみてください。

このように、返り値であるpromiseオブジェクトのthenメソッドで連ねていくと、1本道のステップを同期・非同期に関係無く簡単に作ることができます。

更に、qは「非同期処理を並列実行し、2つの処理が終わった直後に、2つの返り値を利用して処理を続行する」という複雑な処理も、簡単に書くことができます。

var Q = require('q');

// ステップ1のA。2000ms 後に、文字列を返す。
var step1A = function() {
    var d = Q.defer();
    setTimeout(function() {
        d.resolve('result of step1A');
    }, 2000);
    return d.promise;
};

// ステップ1のB。700ms 後に、文字列を返す。
var step1B = function() {
    var d = Q.defer();
    setTimeout(function() {
        d.resolve('result of step1B');
    }, 700);
    return d.promise;
};

// ステップ2。並列処理の結果を出力する。
var step2 = function(step1A_result, step1B_result) {
    console.log(step1A_result + ', ' + step1B_result);
}

// ステップ1A と、ステップ1B を並列実行する。
Q.allResolved([step1A(), step1B()])

// 両方の処理が終わったらすぐに、ステップ2 を実行する。
.spread(step2);

これを自前でやろうとすると、おかしなコードになりがちです。2つの並列処理の最後に、お互いの処理が既に終わっているかどうかをチェックし、trueなら第3の処理を続行。しかし2つの処理結果の引数を渡しにくいし、順序も分かりにくいし、コードも重複しがちです。さらに、並列処理が10個だとどうでしょうか? きっと何もかもが嫌になり家に帰って10時間眠りたくなるでしょう。qがあれば、安全かつ最速でステップを進めることができます。

安全に、と言えば、qはステップの途中に処理を離脱するコードも1カ所に書いておけるのも魅力です。

var Q = require('q');

// ランダムで、OKあるいはNGになるステップを作成。
var okOrNgStep = function() {
    var d = Q.defer();
    var timeout = Math.random() * 1000;
    setTimeout(function() {

        // 1:1 の確率で、OK あるいは NG と判断します。
        timeout > 500 ? d.resolve('More than 500!') : d.reject('timeout <= 500..');
    }, timeout);
    return d.promise;
};

Q.when()

// OKあるいはNGになるステップを実行。
.then(okOrNgStep)

// OKなら、こちらにステップが進む。
.then(function(msg) {
    console.log('OK! ', msg); 
})

// NGなら、こちらにステップが進む。
.fail(function(msg) {
    console.log('NG... ', msg);
})

// 最後には必ずここに来る。
.done(function() {
    console.log('... done.');
});

これは、ステップのどこかで起きた例外のハンドリングのためにとても有用です。仮に10ステップの長いフローがあったと考えてみてください。もしこの機能が無ければ、全てのステップの中に if (err) handleError(err); と書いておかねばならないでしょう。

q以外にも、非同期ユーティリティは他にもたくさんあるようです(https://github.com/joyent/node/wiki/modules#wiki-async-flow)。しかし私が触ったいくつかのnpmモジュールは qを使っていたので、比較的広く導入されているんじゃないかと思います。

個人的には、引数の取り方が柔軟過ぎる気もしますが、それほど問題にならないのかも知れません。

あなたは非同期管理をどうしていますか?