coffeescriptとstep.js でどれぐらい非同期を同期的に簡潔に書けるか?

なんか日本語がおかしいですが…
nodejs/expressの習作として、簡単なマイクロブログ作ってたんですが
MongoDBのORMとしてMongooseを使ってて、DBの呼び出しってNodeJSでは基本的に非同期なので
たしかにnodeの設計思想からしてそうすべきだとは思うんですが、単純にコードとしての見栄えが悪くなってました

で、js直には触らず、全部 coffeescript で書いてるんですが
たとえば、expressとmongooseに突っ込むところを組み合わせると、こうなります

require 'coffee-script'
mongoose = require 'mongoose'
express = require 'express'
OAuth = require('oauth').OAuth

# 中略
# OAuht認証のところだけ抜粋
app.get '/oauth_verify',(req, res)->
  oauth_token = req.query.oauth_token
  oauth_verifier = req.query.oauth_verifier
  if oauth_token and oauth_verifier and req.session.oauth
    oauth.getOAuthAccessToken oauth_token, null, oauth_verifier,
      (error, oauth_access_token, oauth_access_token_secret, results) ->
        req.session.regenerate ()->
          req.session.name = results.screen_name
          req.session.twitter_id = results.user_id
          User.findOne { id:results.user_id } , (err,user)->
            if not user
              item = new User()
              item.name = results.screen_name
              item.id = results.user_id
              item.twitter =
                token: oauth_access_token
                token_secret: oauth_access_token_secret
              item.save (err)-> console.log 'add new user:'+JSON.stringify(item)
            console.log "[login] #{results.screen_name}" if user
            res.redirect '/'
  else
    oauth.getOAuthRequestToken (error, oauth_token, oauth_token_secret, results)->
      req.session.oauth =
        oauth_token: oauth_token
        oauth_token_secret: oauth_token_secret
        request_token_results: results
      res.redirect 'https://api.twitter.com/oauth/authorize?oauth_token=' + oauth_token

node.js+socket.io+oauth+SessionWebSocketでログイン付きチャットを作るメモ - すぎゃーんメモを参考に、coffeescriptで書きました
コード全体は express/coffeescript ― Gist に置いておきます


どうでしょう、expressとmongooseのコールバックで奥へ奥へと… まあ悪かないんですが、プログラマたるもの、単純にネストが深いコードは警戒してしまうわけです
まあ、元々簡単に書けてしまう部分でもないんですが、だからこそシンプルにしておきたい、ですよね

っていうので、そういえば非同期を上手く記述するライブラリがあるよなーと、色々触ってみたんですが、
一番シンプルそうだった step.jsってのに落ち着きました。

step.js

creationix/step - GitHub
インストールは npm で npm install step

やってることは単純で、コールバック関数に this を代入してやると、次の関数呼び出しではそこから処理が開始されます。
公式のサンプルコードはこんな感じです

Step(
  function readSelf() {
    fs.readFile(__filename, this);
  },
  function capitalize(err, text) {
    if (err) throw err;
    return text.toUpperCase();
  },
  function showIt(err, newText) {
    if (err) throw err;
    console.log(newText);
  }
);


便利なstep.jsなんですがcoffeeescriptと組み合わせるときはちょっと工夫がいります
coffeescriptは、関数の中では「最後に評価されたものをreturnする」って仕様になってて、
これはこれでメソッドチェーンとか書きやすくはなるんですが、step.jsではvoid以外でreturnされると、そこで挙動が変わってしまいます
だから、明示的に空のreturnを書いてやる必要があります。ちょっとカッコ悪いですね。

という感じで、OAuthのコードを描き直してやるとこうなります

app.get '/oauth_verify',(req, res)->
  oauth_token = req.query.oauth_token
  oauth_verifier = req.query.oauth_verifier

  if oauth_token and oauth_verifier and req.session.oauth
    _atoken = _asec = ""
    _res = {}

    step ()->
      oauth.getOAuthAccessToken oauth_token, null, oauth_verifier,@
      return
    , (e, oauth_access_token, oauth_access_token_secret, results) ->
      _atoken = oauth_access_token
      _asec = oauth_access_token_secret
      _res = results
      req.session.regenerate @
      return
    , ()->
      User.findOne { id:_res.user_id } ,@
      return
    , (e,user)->
      console.log user
      if not user
        item = new User()
        item.name = _res.screen_name
        item.id = _res.user_id
        item.twitter =
          token: _atoken
          token_secret: _asec
        item.save (e)-> console.log 'add new user:'+JSON.stringify(item)

      req.session.name = _res.screen_name
      req.session.twitter_id = _res.user_id
      console.log "[login] #{_res.screen_name}"
      res.redirect '/'
      return
  else
    oauth.getOAuthRequestToken (error, oauth_token, oauth_token_secret, results)->
      req.session.oauth =
        oauth_token: oauth_token
        oauth_token_secret: oauth_token_secret
        request_token_results: results
      res.redirect 'https://api.twitter.com/oauth/authorize?oauth_token=' + oauth_token

ネストは減りましたが、縦に長くなってしまいました。
あと、step.jsで実行されるそれぞれの名前空間は、step関数が呼び出された時点に依存していて、関数の中の名前空間は、そのクロージャで閉じているので、
関数から関数へ値を渡したいときは、バッファーとなる変数を用意しとく必要がありました。_atoken,_asecret,_res がそれです


結論として

  • ちょっとネストが深くなるのは許容する
  • ネストが3段超えた辺りでstep.js使うのを検討する


っていう風にすると可読性が高くなるんじゃないかなって思いました。まる。


他の非同期ライブラリとかは、InfoQ: 仮想パネル: JavaScriptで非同期プログラミングを乗り切る方法が、それぞれの設計思想とかまとまって良かったです。