Rendr徒然

これは「Node.js Advent Calendar」7日目の記事です。

去年のアドカレ (東京Node学園祭用で12月じゃなかったけど) 以来、リリースの告知エントリ一つしか書いてなかったよ。ブログだけじゃなくTwitterもたいしてつぶやいてないし、ネットからのフェードアウトが進んでおりまする。あー、なにも残さず消えるように死ねたらいいのに。実際には今死ぬと膨大なゴミ (大半は書籍と雑誌だけど) を残すので早く処分しないと死ぬに死ねない。


ともあれ (JW)、今回はRendrについてダラダラと書きます。まとまりなくてごめんなさいです。
自分がRendrを知ったのは、@mshkさんによるQiitaの記事からでした。

同じく@mshkさんによる東京Node学園祭のRendr紹介セッションの資料はこちら。

Qiitaの最初の記事は結構注目を集めたようで、自分もそれを見て実際にRendrを触ってみて、これはいいと思い実戦投入するなどしたのですが、その後あまりRendrの情報が増えなくて残念な感じです。フェードアウト中のお前が言うなって感じですが。そんなわけで (どんなわけで?)、始めます。

RendrのMV*的な構造

前述のリンク先等で紹介されているように、RendrはBackboneをクライアントのみならずサーバでも動かすためのライブラリです。BackboneはいわゆるMVCフレームワークではあるものの、古典的なMVCそのものではなく、DOMイベントのハンドリングをViewで行う (ViewがControllerを兼務する) 一方、Controllerそのものに該当するものはありません。また、ViewはViewでどう作るかはアプリケーションの開発者に丸投げ感があります。Backboneは文字通り背骨だけ、骨格の一部だけを提供する薄いフレームワークなのですね。

Rendrはその薄いBackboneをそのままサーバでも動かすというわけではなく、いくつか追加の構造を与えます。具体的には、

  • Controllerがある。
  • レンダリングにはテンプレートエンジン (デフォルトはHandlebars) を使う。

Controllerがあるといっても、BackboneのViewで行うDOMイベントのハンドリングを担うものではありません。そうではなく、URLの変化、今時のブラウザだと popstate イベントに反応して、次に表示するためのModelを用意するのがRendrのControllerです。

図にするとこんな感じ (スマホとか画面の幅が狭いとまともに見えないかも、ごめんちゃい)。

+-----+  popstate  +------------+  create  +-------+
| BOM |----------->| Controller |--------->|       |
+-----+            +------------+          |       |
                                           |       |
+-----+   event    +------------+  update  |       |
|     |----------->|    View    |--------->| Model |
|     |            +------------+          |       |
| DOM |                                    |       |
|     |   render   +------------+   read   |       |
|     |<-----------|  Template  |<---------|       |
+-----+            +------------+          +-------+

Backboneでpopstateイベントを扱っているのはRouterですが、それは昔 (v0.5より前) Controllerという名前だったとか。RendrのControllerはRouterの先で呼び出される (というか、クライアントサイドではControllerの this はRouterですw) ので、先祖返りといった趣ですね。

URLにどのControllerが対応するかは、app/routes.jsで定義します。たとえばサンプルの場合はこんな感じ。

module.exports = function(match) {
  match('',                   'home#index');
  match('repos',              'repos#index');
  match('repos/:owner/:name', 'repos#show');
  match('users'       ,       'users#index');
  match('users/:login',       'users#show');
  match('users_lazy/:login',  'users#show_lazy');
};

まるでサーバサイドのWebフレームワークでコントローラ/アクションを定義しているみたいですね。実際、RendrのControllerはまさにサーバサイドのWebフレームワークにおけるControllerそのものです。なお、現在のRendrはモジュールシステムとしてCommonJSを使います (RequireJS対応のプルリクも進行中)。そして、その配置や命名に少しばかり規則があります。たとえば 'users#show' の場合は、app/controllers/users.js というモジュールで定義されているControllerの、show() メソッドが呼び出されます。また、それに対応するViewはapp/views/users/show.js、テンプレートはapp/templates/users/show.hbsのようになります。

サーバサイドで動く時のRendrはこんな感じになります。

+-----+   GET    +------------+  create  +-------+
|     |=========>| Controller |--------->|       |
|  B  |          +------------+          |       |
|  r  |                                  |       |
|  o  |          +------------+          |       |
|  w  |          |    View    |          | Model |
|  s  |          +------------+          |       |
|  e  |                                  |       |
|  r  |   HTML   +------------+   read   |       |
|     |<=========|  Template  |<---------|       |
+-----+          +------------+          +-------+

ControllerがサーバサイドWebフレームワークのControllerそのままだということが一目瞭然ですよね。この書き方だとViewが宙ぶらりんな感じですが、実際は使用するテンプレートを決めたり、テンプレートに渡すデータ (Model) を整えたりします。Viewのメソッドはそれらのようにクラサバ両方で実行されるもの (DOMに依存しない) と、クライアントでのみ実行されるもの (DOMに依存できる) があるので、@mashkさんの「Rendr入門(1)」やGitHubのドキュメントというかreadmeを見ておくといいでしょう。

テンプレートがサーバサイドで実行される場合は、その時のModelやViewをクライアントサイドで復元するための情報も一緒にレンダリングします。これによって、ブラウザではあたかも最初からそこで動いていたかのようにBackboneアプリが動作することができるのです。ただし、この部分 (と、Modelのキャッシュ) はシンプルなRendrの中ではちょっとばかりトリッキーなところで、トラブルの元になりやすい印象です。アプリ内で画面遷移 (Router.navigate()) した後はうまく動くのに、リロードするとうまく動かない、なんて場合はそこら辺ではまってる可能性があります。

なお、サーバサイドでBackboneが動く時というのは、(ブックマーク等から) URLが直接叩かれた場合や (F5等で) リロードされた場合などで、HTTPメソッドとしてはGETのみとなります。なので、app/routes.js で定義するのもGETメソッドでアクセスされるURLだけです。

RendrはUI層

RendrはBackboneをクライアントでもサーバでも実行するためのライブラリですが、ここでのサーバはサーバといっても、基本的にUI層です。つまり、Backboneを使ったシングルページアプリケーションの典型的なシステム構成がこんな感じだとして、

  client               server
==========         ==============

+---------+  HTTP  +------------+
| Browser |<======>| API server |
+---------+        +------------+

Rendrを導入するとこうなるということです。

  client                           server
==========         ======================================

                UI層                         サービス層
===================================        ==============

+---------+  HTTP  +--------------+  HTTP  +------------+
| Browser |<======>| Rendr server |<======>| API server |
+---------+        +--------------+        +------------+

Rendrは基本的に、バックエンドのサービス層でBackboneを使う (Backbone.Modelにビジネスロジックを実装するなど) ことを意図したライブラリではないということです。これは、Rendr開発元のAirbnbが元々Railsで実装されたサーバを持っていて、モバイル用のフロントエンドを構築する際、シングルページアプリケーションの初期表示を改善するためにRendrを開発したという経緯があるためでしょう。

Rendrを使うわけではなくても、このようにサービス層を小さく分割してAPI化し、UI層でマッシュアップする的なアーキテクチャは、特にユーザ数の多いサービスで頻繁に見かけますね。そのUI層として、複数のAPIを並行に呼び出すのが簡単といった理由でNode.jsが選ばれるケースも多く見かけるようになってきて、最近もWalmartとかGrouponとかがそれっぽい感じです。そんなアーキテクチャでシングルページアプリもクライアントの一つとして提供するなら、Rendrはピッタリとマッチするでしょう。

Rendrとバックエンドサービス

Rendrがバックエンドのサービスとどのようにコミュニケートするか、Backboneがサーバサイドで実行される場合とクライアントサイドで実行される場合、それぞれで見てみましょう。まずはサーバサイドの場合。

Browser                                 Rendr server                             Backend
=======          ======================================================        ===========

+-----+   GET    +------------+    +-------+    +------+    +---------+        +---------+
|     |=========>| Controller |--->|       |    |      |    |         |        |         |
|  B  |          +------------+    |       |    |      |    |         |        |         |
|  r  |                            |       |    |      |    |         |        |         |
|  o  |          +------------+    |       |    |      |    |         |  HTTP  |         |
|  w  |          |    View    |    | Model |<-->| Sync |<-->| Data    |<======>| Backend |
|  s  |          +------------+    |       |    |      |    | Adapter |        |         |
|  e  |                            |       |    |      |    |         |        |         |
|  r  |   HTML   +------------+    |       |    |      |    |         |        |         |
|     |<=========|  Template  |<---|       |    |      |    |         |        |         |
+-----+          +------------+    +-------+    +------+    +---------+        +---------+

BackboneのModelは fetch()save() が呼ばれると、Syncというオブジェクトを経由してバックエンドとModelの内容をやり取りします。Backbone標準ではXHR経由でRestfulなWeb APIとやり取りしやすいSyncの実装が提供されています。

RendrはこのSyncを置き換えます。サーバサイドでは、RendrのSyncはDataAdapterと呼ばれるオブジェクトを直接呼び出します。DataAdapterはサーバサイド版XHRといった感じのモジュールで、標準ではRequest (mikeal/request)を使ってRestfulなWeb APIとやり取りするためのRestAdapterという実装が用意されています。

Backboneがクライアントサイドで実行される場合はもう少し複雑になりますが、このようになります。

                     Browser                                     Rendr server                  Backend
==================================================        ===========================        ===========

+-----+    +------------+    +-------+    +------+        +----------+    +---------+        +---------+
| BOM |--->| Controller |--->|       |    |      |        |          |    |         |        |         |
+-----+    +------------+    |       |    |      |        |          |    |         |        |         |
                             |       |    |      |        |          |    |         |        |         |
+-----+    +------------+    |       |    |      |  HTTP  |          |    |         |  HTTP  |         |
|     |    |    View    |<-->| Model |<-->| Sync |<======>| ApiProxy |<-->| Data    |<======>| Backend |
|     |    +------------+    |       |    |      |        |          |    | Adapter |        |         |
| DOM |                      |       |    |      |        |          |    |         |        |         |
|     |    +------------+    |       |    |      |        |          |    |         |        |         |
|     |<---|  Template  |<---|       |    |      |        |          |    |         |        |         |
+-----+    +------------+    +-------+    +------+        +----------+    +---------+        +---------+

クライアントサイドでは、RendrのSyncは標準のBackbone.Sync経由でXHRを使ってサーバとやり取りします。その際、たとえばModelが要求したURLが /users だとすると、/api/-/users のように変形したURLをリクエストします。このようなURLは、Rendrサーバ (これはExpressアプリです) ではApiProxyというミドルウェアにルーティングされます。これは名前の通りAPI呼び出しのためのプロキシで、単純にDataAdapterに委譲するだけです。そしてDataAdapterがバックエンドのサービスとやり取りします。

重要なポイントとして、

  • ブラウザから直接バックエンドのサービスとやり取りすることはありません。Rendrサーバを介します。
  • Backboneアプリがブラウザ上で動作している場合、サーバ側ではBackboneアプリは呼び出されません。

前者に関しては好都合なことが多く、Rendrサーバ上でHTTPセッションを維持して認証などに使うことが出来たり、APIサーバとの通信に必要なAPIキーをクライアントサイドに露出することなくDataAdapterのところで設定出来たりします。

後者に関してですが、RendrはBackboneをクライアントサイドでもサーバサイドでも動かすためのライブラリなのですが、どちらかというと「クライアントサーバで」動かすと認識しておいた方が実際の動作に近いです。ブラウザ側でBackboneが動いてる時のHTTPリクエストではサーバ側でBackboneは動かず、サーバ側でBackboneが動く時はURL直叩きなのでブラウザ側ではBackboneは動いてないということなのです。

サーバサイドJavaScriptのメリットとしてクライアントとサーバでコードを共有できるという期待が語られたり、実際にはバリデーションくらいしか共有出来ないという現実が語られたりします。RendrだとあたかもBackboneのModel (通常バリデーションはここで行う) も含めてクラサバでの共有がより進みそうな印象を持つ人もいるかも知れません。実際、ModelもViewもControllerも (テンプレートも) クラサバで共有されるのですが、それはあくまでもレンダリングのためです。

特にバリデーションについては、サーバサイドでは基本的に実行されません。サーバサイドでBackboneが実行される時というのは、前述のようにURL直叩きやリロード時であり、GETメソッドによるリクエストの時だけです。その場合、通常のBackboneアプリでは Model.fetch() が呼ばれることはあっても Model.save() が呼ばれることはなく、従ってバリデーションが実行されることもありません (変わった実装をしていない限り)。

ちなみに、RendrではクライアントサイドのSyncはリクエストにemulateJSONを使います。つまり、ブラウザからRendrサーバへのリクエストではコンテントタイプは application/json ではなく、application/x-www-form-urlencoded になります。理由は不明ですが、Aribnbがサポートしているデバイスの都合でしょうか?

ふたたびRendrはUI層?

このように、Rendrというのは名前の通りサーバサイドでもレンダリングを行うためのライブラリです。そのため、Rendrの標準的な構成ではDataAdapter (RestAdapter) を通じてバックエンドのサービスを利用することになります。

しかし、冒頭でも紹介した「Rendr入門」の @mshk さんは、DataAdapterのところでMySQLへのアクセスを含む、いわゆるビジネスロジックを実装しているようです。AirbnbのようにRendr以前からバックエンドのサーバが存在している場合はともかく、全部新規のサービスだとわざわざHTTPアクセスを挟んでレイテンシを増やしてまでUI層とサービス層をわける理由がないということでしょう。しかし少なくとも現時点では、DataAdapterの先でデータベースアクセスを含むビジネスロジックを実装するのはRendrのユースケースから外れていて苦労しているようでした。たとえばDataAdapterでURLを見て自前でルーティング的なことをしないといけないなどなど。

また、RendrのGitHubリポジトリに早い時期からプルリクエストなどしていたアーリーアダプターの一人 @tak0303 (GitHubでは @takashi) さんも、同じように困っている模様。また、DataAdapterのところからデータベース (MongoDB) へアクセスしてもいるようです。MongoDBのようにHTTPでアクセス可能なDBはDataAdapter経由でアクセスすることも容易ですが、その場合はModelにビジネスロジックを実装することになるのでしょうか。通常のBackboneアプリ (ブラウザのみで実行される) から直接MongoDBにアクセスするような感じ? クライアントサイドはいたずらしやすいので認証・認可とかしっかりしていないと怖い気がしますが、DynamoDBなんかもブラウザから直接アクセスできるようになってきてるし (これとかこれ)、この方向もありなのかもしれませんね。

サーバサイドでこの先生きのこるには?

思い返せば、Node.jsは当初「サーバサイドJavaScript」として注目を集めたのでした。それから数年が経過し、今のNode.jsはサーバサイドではなく専らコマンドラインの世界で使われています。特にGruntの普及がめざましく、そのせいかnpmレジストリの負荷がこの一年で劇的に増加しているそうです。一方サーバサイドでは、主な用途として期待されたWebSocketの使い道が今ひとつ見つからず、かといって典型的な (RDBにアクセスしてHTMLを返す) Webアプリでは大きなメリットも見いだせず、といった状況でした。

そんな中、RendrはサーバサイドでNode.jsを使うメリットを鮮やかに見せてくれました。現在のRendrはBackboneべったりな作りですが、重要なのはBackboneかどうかではなく、同じフレームワークを使ってクライアントとサーバの両方でレンダリングするというアーキテクチャの方です。実際、Rendrの作者Spike Brehmのプレゼン資料「General Assembly Workshop: Advanced JavaScript」では、Backboneを使わずにRendr的「Isomorphic」なアーキテクチャを解説しています (チュートリアルもあり)。つまり、AngularJS版やEmber.js版のRendrがあってもいいわけですね。実現性は怪しいですが、Rendr自体にもBackbone依存を無くしてAngularJSなどへ対応しやすくするというIssueがあったりします。

Rendrや同じようにIsomorphicなアーキテクチャのクライアントサイドMV*なフレームワークが普及すれば、大規模なサービスにおけるサーバサイドのUI層として、あるいはより小規模な環境でのthinサーバとして、Node.jsが使われる機会が増えるかなと期待しています。が、シングルページアプリ自体が話題の割に今ひとつ広まらない (スマホもネイティブアプリ一辺倒になってるし) 印象もあり、果たしてサーバサイドJavaScriptとしてのNode.jsがこの先生きのこることができるのか、心配で夜も眠れない日々を過ごしています(日中はよく眠れてしまうので出勤時間的に非常にまずいです)。


明日はid:mizchiさんです。


P.S.

  • はてな記法を忘れてて書くのが大変だった。
  • Cacooで図を書き始めたけど必要以上に凝りそうになって今日中に終わらなくなりそうだったのでやめた。


P.P.S

他のアドカレをまねてRendrに送ったプルリクをまとめてみるよ。といっても4つしかないけど。

#111はこの中で一番役に立ったプルリクだと思う。当時のRendrはサーバサイドのControllerで複数のModel/Collectionを用意しても、レンダリング後にクライアント側で復元されるModel/Collectionは一つだけでした。#111はそれを修正して複数のModel/Collectionを復元出来るようにしたもの。マージされた際にはRendrの公式アカウントでツイートされて嬉しかったです。



#115はバックエンドからのHTTPステータスがブラウザに返されなくて (ステータス200になってしまう)、ステータス204などレスポンスボディがない場合にjQueryのXhrでエラー扱いされてしまうのを修正したもの。Rendrを実戦投入した案件ではステータス204を使うことになっていたので、マージしてもらえてよかったです。

#142はModel.fetch()でエラーとなった場合の詳細情報をContollerから取得しやすいようにしたもの。そこで別の (エラー用の) 画面に遷移するなどのハンドリングがしやすくなりました。

最後の#143はどうでもいい些細なものですね。Rendrの中の人 (Spike) は割とプルリクをマージしてくれるので (受け入れ過ぎじゃないかと思うくらい)、みんなもどんどんプルリクしたらいいと思うよ!!


最初にプルリクした当時はRendrのGitHub issueは静かなものだったのですが、最近は連日たくさんのIssueコメントが付いて、スターしてると飛んでくるメールも結構な量になります。といっても1日10通前後かな? Node.jsみたいにそれだけで毎日数十通ということはないけれど、結構な人気ぶりです。国内でももっとRendrの話題が盛り上がるといいですね。

Seasar2.4.47 リリース

しました.

ソースコードリポジトリGitHubに移行しています。

■変更点

Seasar2.4.46 からの変更点は次のとおりです.

  • Bug
    • [CONTAINER-443] - [S2JDBC-Gen] TIME型のデータがdump-dataとload-dataで復元できない問題を解決しました
    • [CONTAINER-444] - [S2DBCP] コネクションプールへの返却処理中に物理コネクションのsetAutoCommitメソッドから例外がスローされると返却処理が適切に行われない問題に対応しました。
    • [CONTAINER-445] - [S2JDBC-Gen] Gen-Ddlタスクが生成するCREATE TABLE文のカラムの並び順がJava 6と7で異なる問題に対応しました
    • [CONTAINER-447] - [S2JDBC] Jenkins 1.500でテストが失敗するのを修正しました。[operation:2694]
    • [< href='https://github.com/seasarorg/seasar2/pull/3'>GH-3] - [S2Container] 正規表現として扱われる文字列内にあるピリオドをエスケープしました。
  • New Feature
    • [CONTAINER-446] - [S2JDBC] AutoSelect で取得するカラムを指定できるようにしました。[Seasar-user:21654]
    • [GH-1] - [S2-Tiger] DBの4桁のカラムに5桁のテストデータを投入している箇所を修正しました
    • [GH-2] - [S2JDBC-Gen] 開発途中で止まってずっと放置されている maven-s2jdbc-gen-plugin プロジェクトを削除しました


■移行の注意点 (今回はありません)


■ダウンロードはこちらからどうぞ.


Maven2からのご利用はこちらを参照ください.

文字ストリームと StringDecoder

この記事は「東京 Node 学園祭 2012 アドベントカレンダー」の 15 日目の記事です。

id:jovi0608 によるこのアドカレ 13日目のエントリ「Node API のクラス図を公開しました。」でも明らかなように、Node の重要なコンセプトの一つがストリームです。ストリームについては id:Jxck による東京 Node 学園の発表資料 「Node Academy 7 | ”Stream Stream Stream !!!”」も参考にどぞー。

そのストリームですが、大雑把には入力ストリーム (Readable Stream) と出力ストリーム (Writable Stream) があり、その両方でもあるフィルタストリーム (Filter Stream) や 双方向ストリーム (Duplex Stream) がある。。。 なんて話が上記の id:Jxck による資料には書いてあったりします。それとは別の観点での分類として、ストリームにはバイトストリームと文字ストリームがあると考えることができます。といっても、実装クラスとして分類できるというわけではありません。単に、ストリームを流れるデータがバイト列か文字列か、というだけです (他にも任意の JavaScript オブジェクトを流す、オブジェクトストリームなんかもありますが、このエントリでは扱いません)。

出力ストリームを文字ストリームとして扱うには、write() メソッドに string の値を渡します。同時にエンコーディングを渡すこともできて、デフォルトは UTF-8 です。

os.write('hoge hoge', 'utf8');

出力ストリームの場合、write() のたびに Buffer を渡すことも文字列を渡すこともできます。つまり、バイトストリームか文字ストリームかという明確な状態は持たないということになります。

一方、入力ストリームを文字ストリームとして扱うには、setEncoding() メソッドでエンコーディング方式を指定します。エンコーディング方式を省略した場合は UTF-8 になります。

is.setEncoding('utf8');

一度 setEncoding() を呼び出して文字ストリームとすると、それをバイトストリームに戻す API は提供されていません。引数無しで setEncoding() を呼び出すことでバイトストリームになるようにしようという提案 (#3643) がされていて、HTTP モジュールの API ドキュメントには (間違って) そう書かれていたこともあるのですが、今のところマージされていません (つーか、ストリームはたくさんあるのに net.Socket だけ直すってどんだけー)。必要だと思う人は @isaacs の Streams2 が master にマージされる前後が大チャンスなので (たぶん)、プルリクエストするなり #3643 にコメントするなりしませう。

なお、全ての入力ストリームが文字ストリームになれるわけではありません。たとえば zlib モジュールは現在のところ文字入力ストリームに対応していません。ただし、現在開発中の Streams2 では入力ストリームのベースクラスが setEncoding() メソッドを持っているので、いずれは zlib も文字入力ストリームとして使えるようになると思われます。

このエントリではもっぱら文字入力ストリームについて書いていきます。

まず、なぜ文字ストリーム、とりわけ文字入力ストリームが必要なのでしょうか? それは、文字入力ストリームがないと不便で、バグの元になりやすいからです。たとえば、受信したバイト列を文字列に変換してログ出力する例:

is.on('data', function(buf) {
  console.log('受信データ : ' + buf.toString());
});

このコードは受信したデータが ASCII のように、一文字が 1 バイトにエンコードされたバイト列の場合はうまく動きます。しかし、UTF-8 のように複数バイトにエンコードされている場合、そしてそれが複数のチャンクに分割されている場合には、うまく動かない場合があります。たとえば 'あいう' という文字列は、UTF-8 では

E38182 E38184 E38186

という 9 バイトにエンコードされます。これが先頭から 4 バイトと残り 5 バイトの二つのチャンクに分割された場合、'い' を正しく文字列化することができません。
同じ状況を REPL で簡単に試すと:

> buf = new Buffer('あいう')

> console.log('受信データ : ' + buf.slice(0, 4).toString())
受信データ : あ�
> console.log('受信データ : ' + buf.slice(4, 9).toString())
受信データ : ��う

このように、まるで文字化けしたような出力になってしまいます。Node の初期の実装 (0.1.95 まで) では、入力ストリームは上記のような実装になっていたため、UTF-8エンコーディングされた文字列を正しく扱うことができませんでした。

そこで必要になるのが文字ストリームで、そのために使われるのが string_decoder モジュールの StringDecoder クラスというわけです。StringDecoder は、UTF-8 のように複数バイトでエンコーディングされたバイト列を受け取ると、それが文字の境界で分割されていないかチェックし、もし分割されて不完全なバイト列で終わっていると、その手前の (文字として完全な) バイト列までを文字列に変換します。そして残りのバイト列を覚えておいて、次に受信したバイト列と連結することで、正しく文字列に変換します。
REPL で簡単に試すと:

> decoder = new string_decoder.StringDecoder('utf8')
> console.log('受信データ : ' + decoder.write(buf.slice(0, 4)))
受信データ :あ
> console.log('受信データ : ' + decoder.write(buf.slice(4, 9)))
受信データ :いう

このように、最初の呼び出しで 4 バイトを渡しても、先頭の 3 バイトである 'あ' だけが文字列に変換されて、残りの 1 バイトは次の呼び出しで渡した 5 バイトと連結されて 'いう' に変換されたことがわかります。これにより、前述のコード片や

var data = '';
is.setEncoding('utf8');
is.on('data', function(str) {
  data += str;
});

のように受信した文字列を全部連結して、'end' イベントでまとめて扱うようなコードが UTF-8 でも安全に動作するようになりました。

StringDecoder は最初 Utf8Decoder という名前で、後のコアメンバーである Felix Geisendörfer によって実装されて 0.1.96 にマージされました。名前のように UTF-8 専用だったのですが、0.1.99 で現在の StringDecoder に変更されてその他のエンコーディングに対応しました。といっても、当時の Node が UTF-8 以外にサポートしていたエンコーディング方式は、ASCII ('ascii') と現在は deprecated ということになっている (でも無くさないでくれという要望が根強い) バイナリ ('binary') という、1 バイトへのエンコーディング方式だけだったのですが。

その後、v0.4.0 で Node は UCS-2 をサポートするようになりました。これもドイツの方の貢献によるのですが、そのパッチに含まれていたのは Buffer の対応だけで、StringDecoder は変更されませんでした。そのため、setEncoding('ucs2') はうまく動きませんでした。
v0.6.21 の REPL で試すと:

> buf = new Buffer('あいう', 'ucs2')

> decoder = new string_decoder.StringDecoder('ucs2')
{ encoding: 'ucs2' }
> console.log(decoder.write(buf.slice(0, 3)))
あ
> console.log(decoder.write(buf.slice(3, 6)))
䘰

まともにデコードできません。しかし、少なくともデータ交換で使われるエンコーディング方式として現在は UTF-8デファクトであり、UCS-2 が使われることはほとんどなかったのでしょう。Node の issue でも要望されたことはなかったような気がします。

それから 1 年以上が経過して、v0.8.0 から Node はサロゲートペアを含む UTF-16 に対応するようになりました。現在の Node では、UCS-2 は UTF-16LE のエイリアスという扱いになっています。そのときにようやく、StringDecoderUCS-2/UTF-16LE に対応しました。需要があったのかどうかは不明ですが。。。
v0.8.14 の REPL で試すと:

> decoder = new string_decoder.StringDecoder('ucs2')
> console.log(decoder.write(buf.slice(0, 3)))
あ
> console.log(decoder.write(buf.slice(3, 6)))
いう

もちろん、UTF-16 に対応しているので、サロゲートペアの途中で途切れたチャンクでも大丈夫です。

> buf = new Buffer('お𠮟り', 'utf16le')

> decoder = new string_decoder.StringDecoder('utf16le')
> console.log(decoder.write(buf.slice(0, 4)))
お
> console.log(decoder.write(buf.slice(4, 8)))
𠮟り

ところで、UTF-8サロゲートペアはなく、BMP (基本多言語面U+0000U+FFFF) に含まれない文字は 4 バイトでエンコーディングすることになっているのですが、世の中には UTF-8 の文字は 3 バイトまでということに依存していたりするのか、UTF-16 におけるサロゲートペアの上位・下位の各 16bit をそれぞれ 3 バイト、計 6 バイトで表現する CESU-8 と呼ばれるエンコーディング方式もあるらしいです。Oracle とか Oracle とか Oracle で使われていると Wikipedia に書いてありました。UTF-8 としては RFC 違反なわけですが、サロゲートペアに対応していない UCS-2 -> UTF-8 変換ツールを使って UTF-16 を変換しちゃったりすると、CESU-8 なバイト列ができあがっちゃいます。んで、なぜか Node で利用されている JavaScript エンジン、V8 は CESU-8 なバイト列でも (内部形式である) UTF-16LE に変換してくれるので、Node の StringDecoder でも UTF-8 といいながら CESU-8 なバイト列が一文字 (最長 6バイト) の途中で分断されても大丈夫なようになっています。

> buf = new Buffer('eda182edbe9f', 'hex')

> decoder = new string_decoder.StringDecoder('utf8')
> console.log(decoder.write(buf.slice(0, 3)))

> console.log(decoder.write(buf.slice(3, 6)))
𠮟

いわゆる文字コードではないけれど、Buffer がサポートしているエンコーディング方式としては他に HEX ('hex') と BASE64 ('base64') があります。HEX は 1 バイト (たとえば 0x30) を 2文字 (たとえば '30') で表現するので、初期からの StringDecoder でも適切に扱うことができました。

しかし、3 バイトごとのデータを 4 文字で表現する BASE64 の場合は、適切にバイト境界を扱わないと正しくエンコード (文字コードの場合は文字をバイト列にエンコードして、それを文字列にデコードするのですが、BASE64 の場合はバイト列から文字列にする方向がエンコードなのだ) することができません。それだけなら UTF-8 などと同じようにすればいいだけなのですが、BASE64 ではさらにパディングというものがあり、一番最後に 3 バイト未満のデータが残った場合も考慮する必要があります。しかし、従来の StringDecoder にはメソッドが write() しかなく、ストリームの終端で呼び出されるインタフェースがありませんでした。そのため、StringDecoder では BASE64 をサポートしていなかったのですが、最近リリースされたばかりの v0.9.3 からは StringDecoderend() メソッドが追加され (当然、net.Socket などの既存ストリームはそれを呼び出すように変更されています)、BASE64 にも対応しました。

v0.9.3 の REPL で試すと:

S2Util 0.0.1 リリース

しました.

■変更点

S2Util 0.0.0 からの変更点は次のとおりです.

  • Bug
    • [UTIL-1] - CopyUtil#copy() で,ファイル以外の入力ストリームからファイル出力ストリームへコピーすると,出力ファイルが 4KB の整数倍にパディングされてしまう問題を修正しました.[Seasar-user:21260]
    • [UTIL-2] - MessageFormatter を繰り返し使用すると,そのたびに DisposableUtil に Disposable オブジェクトが登録されてしまう問題を修正しました.[Seasar-user:21338]


■ダウンロードはこちらからどうぞ.


Maven2からのご利用はこちらを参照ください.

Seasar2.4.46 リリース

しました.


■変更点

Seasar2.4.45 からの変更点は次のとおりです.

  • Improvement
    • [CONTAINER-441] - [DbSession] セッション ID を保持するクッキーの有効期限を設定できるようにしました.[Seasar-user:21244]


■移行の注意点 (今回はありません)


■ダウンロードはこちらからどうぞ.


Maven2からのご利用はこちらを参照ください.

node-flowless v0.0.2 リリース

しました.

npm install flowlessでインストールすることができます.


v0.0.1 からの主な変更点は次のとおりです.

  • Bug
    • core: seq()par()functions や,map()array が空だった場合のバグを修正しました.
  • Improvement
    • core: map()/runMap() が呼び出す非同期関数の引数並びを Array.map() に近づけました.
    • extras: extras 名前空間に分離しました.
  • New Features
    • core: デバッグログを出力できるようにしました.
    • core: エラーや例外を独自の Error オブジェクトでラップするようにしました.
    • extras: flattenFirst(), flattenSecond(), flattenThird() を追加しました.


map()/runMap() は従来,配列の要素とコールバック関数を引数として非同期関数 function(value, cb) を呼び出していたのですが,id:taedium さんの nue 同様,Array.map() に近づけて以下のようにしました.

  • function(value, cb) (引数が 2 つの場合,従来と同じ)
  • function(value, index, cb) (引数が 3 つの場合)
  • function(value, index, array, cb) (その他の場合)


extras の API は従来,require('flowless') で返ってくるオブジェクトに core と同レベルで公開していたのですが,それを extras オブジェクトにまとめました.なので,使用するには

var extras = require('flowless').extras;

などとする必要があります.


flattenFirst() は最初の引数 (大抵は直前のコールバック関数の 2 番目,エラーの次に渡された引数) として渡された配列をフラットに展開してコールバックを呼び出すユーティリティです.でもあんまり役に立たなさそう.


んで,残りの新機能二つを以下に.

デバッグログ出力


元々 Node のコアライブラリは NODE_DEBUG という環境変数にモジュール名を指定することでデバッグログを出力するようになっています.flowless でも同じようにというか,NODE_DEBUG をそのまんま利用させてもらって,デバッグログを出力するようにしました (実は node-tunnel が既にそうしている).


おなじみのサンプル:

var fs = require('fs');
var flowless = require('flowless');
var extras = flowless.extras;

flowless.runSeq([
  [fs.readdir, __dirname],
  extras.array.filter(function(filename) {
    return /\.txt$/.test(filename);
  }),
  extras.array.map(function(filename) {
    return __dirname + '/' + filename;
  }),
  flowless.map([fs.readFile, flowless.first, 'utf8'])
], function(err, files) {
  if (err) throw err;
  files.forEach(function(file) {
    console.log('-----');
    console.log(file);
  });
});

これを普通に実行すると

$ node ex.js 
-----
FILE1

-----
FILE2

となりますが,NODE_DEBUG=flowless を指定するとこうなります (標準モジュールと同時にログ出力する場合は NODE_DEBUG=http,flowless などと指定することができます).

$ NODE_DEBUG=flowless node ex.js
FLOWLESS: BEGIN runSeq at (/tmp/ex.js:5)
FLOWLESS: begin runSeq[0] at (/tmp/ex.js:5) with: [ [Function: next] ]
FLOWLESS: end runSeq[0] at (/tmp/ex.js:5) with: [ null,
  [ 'ssh-eOFQSasB1028',
    '.ICE-unix',
    'file1.txt',
    '.esd-1000',
    'orbit-koichik',
    'file2.txt',
    'keyring-reWKfY',
    'virtual-koichik.eMDJCS',
    '.X0-lock',
    '.X11-unix',
    'ex.js',
    'pulse-dawZJKwGK88U',
    'pulse-PKdhtXMmr18n',
    'node_modules' ] ]
FLOWLESS: begin runSeq[1] at (/tmp/ex.js:5) with: [ [ 'ssh-eOFQSasB1028',
    '.ICE-unix',
    'file1.txt',
    '.esd-1000',
    'orbit-koichik',
    'file2.txt',
    'keyring-reWKfY',
    'virtual-koichik.eMDJCS',
    '.X0-lock',
    '.X11-unix',
    'ex.js',
    'pulse-dawZJKwGK88U',
    'pulse-PKdhtXMmr18n',
    'node_modules' ],
  [Function: next] ]
FLOWLESS: end runSeq[1] at (/tmp/ex.js:5) with: [ null, [ 'file1.txt', 'file2.txt' ] ]
FLOWLESS: begin runSeq[2] at (/tmp/ex.js:5) with: [ [ 'file1.txt', 'file2.txt' ], [Function: next] ]
FLOWLESS: end runSeq[2] at (/tmp/ex.js:5) with: [ null, [ '/tmp/file1.txt', '/tmp/file2.txt' ] ]
FLOWLESS: begin runSeq[3] at (/tmp/ex.js:5) with: [ [ '/tmp/file1.txt', '/tmp/file2.txt' ], [Function: next] ]
FLOWLESS: BEGIN map at (/tmp/ex.js:13)
FLOWLESS: begin map[0] at (/tmp/ex.js:13) with: [ '/tmp/file1.txt',
  0,
  [ '/tmp/file1.txt', '/tmp/file2.txt' ],
  [Function: next] ]
FLOWLESS: begin map[1] at (/tmp/ex.js:13) with: [ '/tmp/file2.txt',
  1,
  [ '/tmp/file1.txt', '/tmp/file2.txt' ],
  [Function: next] ]
FLOWLESS: end map[0] at (/tmp/ex.js:13) with: [ null, 'FILE1\n' ]
FLOWLESS: end map[1] at (/tmp/ex.js:13) with: [ null, 'FILE2\n' ]
FLOWLESS: END map at (/tmp/ex.js:13) with: [ null, [ 'FILE1\n', 'FILE2\n' ] ]
FLOWLESS: end runSeq[3] at (/tmp/ex.js:5) with: [ null, [ 'FILE1\n', 'FILE2\n' ] ]
FLOWLESS: END runSeq at (/tmp/ex.js:5) with: [ null, [ 'FILE1\n', 'FILE2\n' ] ]
-----
FILE1

-----
FILE2

runSeq() など,flowless が提供する関数の開始時と終了時には大文字で BEGIN/END が出力されます.その中から非同期関数が呼び出される前後には小文字で begin/end/failed が出力されます.


runSeq[n] などの添え字は,runSeq 等に渡された非同期関数の n 番目を実行していることを表しています.また,その時の runSeq() 等の位置を at (file:line) で表示しています.残念ながら,ここで表示される位置は呼び出される非同期関数の記述位置ではありません.それができたら最高なんですけどねー.


nue のように runSeq() に名前を渡せるようにしようかとも思ったのですが,それを必須にはしたくないし,といってオプションにするとその処理でコードが激しく肥大化してしまったので,ひとまず名前は無しにしました.


このログを見ると,runSeq() に渡された関数はそれぞれ begin, end, begin, end... と逐次的に実行されているのに対して,map() に渡された関数は begin, begin, end, end と,二つの引数に対して並行に呼び出されていることが分かります.


また,

FLOWLESS: begin map[0] at (/tmp/ex.js:13) with: [ '/tmp/file1.txt',
  0,
  [ '/tmp/file1.txt', '/tmp/file2.txt' ],
  [Function: next] ]

を見ると,先に書いたように map() に渡した関数には Array.map() と同様の引数 (value, index, array に加えてコールバック) が渡されていることも確認できます.


あまり見やすいログという気は正直しないのですが,十分役に立つ情報にはなっているんじゃないかなと思います.

エラーオブジェクト


flowless では非同期関数から渡されたエラー (Errorインスタンスに限らない) はそのままコールバックに渡していましたが,新しい Error オブジェクトでラップ (っていうの?) するようにしました.


最初は nue のように (というかこれ自体が nue のマネなんですが) 専用のエラークラス (っていうの?) を作ろうかと思ったのですが,Error を継承した (っていうの? プロトタイプをつないだ) クラスを作っても,[[ Class ]] 内部プロパティは Error にはならないのですね.
それで不都合があるかというとよく分からないのですが,なんとなくそれは Error じゃない (Node の util.isError()Object.prototype.toString() 経由でそれを見るので false になる) 気がするので,独自のエラークラスを作るのはやめました.


その上で,Error オブジェクトにいくつかのプロパティを足しています.nue そのままに history プロパティにエラーが発生したところからコールバックのチェーンをたどれる情報も持たせています.


例:

var flowless = require('flowless');

flowless.runSeq([
  function foo(cb) {cb(null)},
  flowless.par([
    function bar(cb) {cb(null)},
    flowless.seq([
      function hoge(cb) {cb(null)},
      function moge(cb) {cb('ERROR')}
    ])
  ])
], function(err) {
  console.log(err);
});

runSeq() の中の par() の中の seq() の中の moge() はコールバックにエラー ('ERROR') を渡しています.
実行すると:

$ node err.js
{ [Error: seq[1] at (/tmp/err.js:7) failed: ERROR]
  cause: 'ERROR',
  history: 
   [ { operator: 'seq',
       index: 1,
       location: '(/tmp/err.js:7)',
       reason: 'failed' },
     { operator: 'par',
       index: 1,
       location: '(/tmp/err.js:5)',
       reason: 'failed' },
     { operator: 'runSeq',
       index: 1,
       location: '(/tmp/err.js:3)',
       reason: 'failed' } ] }

ということで,err.js の 7 行目にある seq が呼び出した 1 番目 (0 オリジンなので二つ目ですね) の関数がコールバックにエラーを渡したことが分かります (ちなみに例外がスローされると failed のところが thrown になります).それは cause から 'ERROR' という文字列であること,history から seq()par() から,それは runSeq() から呼び出されたことも分かります (スタックトレースと同じように上から下へ向かって呼び出し元へ遡ります).


ちなみに,デバッグログも出力するとこうなります.

$ NODE_DEBUG=flowless node err.js
FLOWLESS: BEGIN runSeq at (/tmp/err.js:3)
FLOWLESS: begin runSeq[0] at (/tmp/err.js:3) with: [ [Function: next] ]
FLOWLESS: end runSeq[0] at (/tmp/err.js:3) with: [ null ]
FLOWLESS: begin runSeq[1] at (/tmp/err.js:3) with: [ [Function: next] ]
FLOWLESS: BEGIN par at (/tmp/err.js:5)
FLOWLESS: begin par[0] at (/tmp/err.js:5) with: [ [Function: next] ]
FLOWLESS: end par[0] at (/tmp/err.js:5) with: [ null ]
FLOWLESS: begin par[1] at (/tmp/err.js:5) with: [ [Function: next] ]
FLOWLESS: BEGIN seq at (/tmp/err.js:7)
FLOWLESS: begin seq[0] at (/tmp/err.js:7) with: [ [Function: next] ]
FLOWLESS: end seq[0] at (/tmp/err.js:7) with: [ null ]
FLOWLESS: begin seq[1] at (/tmp/err.js:7) with: [ [Function: next] ]
FLOWLESS: failed seq[1] at (/tmp/err.js:7) with: ERROR
FLOWLESS: failed par[1] at (/tmp/err.js:5) with: seq[1] at (/tmp/err.js:7) failed: ERROR
FLOWLESS: failed runSeq[1] at (/tmp/err.js:3) with: seq[1] at (/tmp/err.js:7) failed: ERROR
{ [Error: seq[1] at (/tmp/err.js:7) failed: ERROR]
  cause: 'ERROR',
  history: 
   [ { operator: 'seq',
       index: 1,
       location: '(/tmp/err.js:7)',
       reason: 'failed' },
     { operator: 'par',
       index: 1,
       location: '(/tmp/err.js:5)',
       reason: 'failed' },
     { operator: 'runSeq',
       index: 1,
       location: '(/tmp/err.js:3)',
       reason: 'failed' } ] }

ということで,結構便利になった気がします.


が,しかし…
ますます less に反して more (肥大化) への道をまっしぐら...

$ wc lib/*
  119   309  2779 lib/extras.js
  255   805  7042 lib/flowless.js
   90   278  2533 lib/utils.js
  464  1392 12354 合計

をいをい… ちょっとダイエットが必要だなぁ.
core (flowless.js) はせめて前バージョンの 150 行を維持,できれば 100 行くらいに縮小したかったんだけどなぁ.デバッグとかエラーとか考え始めると激しく肥大化するのはどんな言語でも同じですね... 無念だ.


次はダイエットを最優先に,テストをもうちょっとちゃんとしたいかな.肥大化したコードにテストが追いついてないので品質に自信が持てない状況なので.
それから extras の整理または廃止など… といいつつ,Stream など EventEmitter 系を混ぜて扱えるようにしたら便利かなとか,ついつい肥大化路線で考えてしまう罠.

node-flowless v0.0.1 リリース

しました.

npm install flowlessでインストールすることができます.


v0.0.0 からの主な変更点は次のとおりです.

  • series()runSeq() に変わりました.
  • makeSeries()seq() に変わりました.
  • parallel()runPar() に変わりました.
  • makeParallel()par() に変わりました.
  • map() および runMap() を追加しました.
  • Extra APIs を追加しました.

API が変わってるから v0.1.0 にしようかと思ったけど,この時期なんて毎回 API 変わるに決まってるから気にしないことにしました.


先のエントリでは

上記の関数は呼び出されると即座に functions で与えられた関数 (series() の場合は functions の先頭要素のみ,parallel() は全部) を実行します.しかし,それだと不便な場合もあるので,非同期関数をフロー制御の元で実行する関数を返す関数も提供しています (日本語が変?).

なんてさらりと書いてしまいましたが,Async のように機能的に完備されたライブラリがあるにも関わらず,劣化版サブセットのようなモジュールを書いた主な理由がこれだったので,むしろこっちをメインに据えた方が差別化がしやすいかなということで,関数を返す方を seq(), par(), map() と簡潔な名前に,そしてこれらのラッパであることを反映して,即時実行の方を runSeq(), runPar(), runMap() という名前にしました.


すぐに実行しないで関数を返すと何が嬉しいかというと,ネストがしやすいのです.こんな感じで自由にネストできます.

var flowless = require('flowless');

flowless.runSeq([
  function one() {...},
  function two() {...},

  flowless.par([
    flowless.seq([
      function three() {...},
      function four() {...},
    ]),

    flowless.seq([
      function five() {...},
      function six() {...},
    ])
  ]),

  flowless.map(
    function seven() {...}
  ),

  function eight() {...}

], function allDone() {...}
);

そして,この入れ子になったプログラムの構造は,フロー制御の流れを直接的に表現します.one() などの中を見なくても,上記のコードが次のようにフロー制御されることが一目瞭然ではないでしょうか.

                                        +-> seven -+
                 +-> three --> four -+  +-> seven -+
    one --> two -+                   +--+-> seven -+-> eight --> allDone
                 +-> five  --> six  -+  +-> seven -+
                                              ...

非同期とか関係なく,普通 (ってなに?) のコードでも個々の詳細を気にしないで全体の流れが見えるようになっているといいですよね.非同期なコードでも全体の流れを表すコードが書きたい,それが flowless のモチベーションの一つです.Slide 譲りの関数テンプレートも匿名関数のノイズを減らして全体像を浮かび上がらせるためなのだ.


そして,簡単にネストできるということは,簡単に組み合わせることが出来るということでもあります.上記の例の one() とか two() をその場でインラインに書くのではなく,別の所で定義して呼び出すのだとすると,それらは次のように書くことができます.

function one() {
  return flowless.seq() {[
    ...
  ]);
}

なので,Node のコア API やいろいろなモジュールが提供する低水準な API を呼び出す関数を flowless で書いて,それらを flowless でつなげて若干高水準な API を作って,さらにそれらを flowless でつなげて高水準なアプリケーションを作る,なんてことも簡単にできるんじゃないかなーと思ってますが,実践で使ってないからなー.絵に描いた餅かもしれません.

map() & runMap()

前回はどうしようかと書いた Map 系ですが,結局コア API として取り入れました.構造化プログラミングの基本三構造である順次・分岐・反復に対抗して,seq(), par(), map() を flowless の基本三構造とします (なんのこっちゃ).


最初は parallelMap() って名前にしていて,将来 sequentialMap() とか追加できる余地を残していたのですが,巨大な配列に対して一斉に非同期関数を呼び出すのってあまりよろしいことではないので,並行に実行する数を指定できるようにしたところ,それを 1 にすれば sequentialMap() になることに気づいたので,名前もシンプルに map() となりました.API 的には

  • map([concurrency,] array, fn)
  • runMap([concurrency,] array, fn, cb)

となります.concurrency のデフォルトは 10 です.array の要素数concurrency より大きい場合は,まず concurrency までの要素を引数として fn が一斉に呼ばれ,それぞれが終わると次の要素を引数に fn が呼ばれます.

Extra API

小さな (less) フロー制御 (control-flow) モジュールで flowless,のはずだったのですが,やっぱりあった方が便利そうなものというのがあれこれあるわけで,それらを Extra API という形で実装しちゃいました.どこが less やねん.心より恥じる.

Extra API といっても内部的にファイルが分かれているだけで,Core API と同様 require('flowless') で使うことができます.前のエントリに追記した際,しれっと出てきた makeAsync() もその一つ.


他には,ArrayString のメソッドのラッパーを集めた array および string というプロパティがあります.
前のエントリで追記したサンプルは次のように書くことができます.

var fs = require('fs');
var flowless = require('flowless');

flowless.runSeq([
  [fs.readdir, __dirname],
  flowless.array.filter(function(filename) {
    return /\.txt$/.test(filename);
  }),
  flowless.array.map(function(filename) {
    return __dirname + '/' + filename;
  }),
  flowless.map([fs.readFile, flowless.first, 'utf8'])
], function(err, files) {
  if (err) throw err;
  files.forEach(function(file) {
    console.log('-----');
    console.log(file);
  });
});

flowless.array.filter() は,前の関数 (fs.readdir() ですね) がコールバックに渡した値 (ファイル名の配列です) を this として,Array.prototype.filter() を呼び出します.次の flowless.array.map() も同様.


あと,generate() ってのも用意しました.これは runSeq() の最初が map() の場合に使うことを想定しています.map() は前の非同期関数がコールバックに渡した配列を引数として受け取る関数を返すのですが,先頭で使われると配列が渡ってこないのですね.

flowless.runSeq([
  flowless.map(function() { // ぐはぁっ
    ...
  }),
  ...
], function(err, result) {
  ...
});

runSeq() の代わりに seq() を使えば

flowless.seq([
  flowless.map(function() {
    ...
  }),
  ...
])([1, 2, 3, 4, 5], function(err, result) {
  ...
});

のように初期値を与えられるのですが,最初の関数に渡される値が最後に出てくるのがいやーんなので,

flowless.runSeq([
  flowless.generate([1, 2, 3, 4, 5]),
  flowless.map(function() {
    ...
  }),
  ...
], function(err, result) {
  ...
});

と書けるようにしたのが generate() です.どこが less やねん.


そして bindFirst() とその仲間達.

  • bindFirst(target, propertyName)
  • bindSecond(target, propertyName)
  • bindSecond(target, propertyName)

これらはそれぞれ,第 1・第 2・第 3 引数を target のプロパティに設定する関数を返します.seq() では非同期関数が呼ばれるたびに値が変換されていく形になるので,その途中で作られた値を後で使いたい場合,どこかへ保存しておかないといけません.それを手軽にするためのものがこれらです.どこが less やねん.


これらを駆使すると,前回のエントリでも書いた asyncblock のサンプルはこうなります.

var fs = require('fs');
var path = require('path');
var flowless = require('flowless');

flowless.runSeq([
  flowless.generate(['path1', 'path2']),
  flowless.map([fs.readFile, flowless.first, 'utf8']),
  flowless.array.join(''),
  [fs.writeFile, path3, flowless.first],
  [fs.readFile, path3, 'utf8']
], function(err, result) {
  if (err) throw err;
  console.log(result);
  console.log('all done');
});

あれ…? なんか厨っぽいぞ?
なんか,LispPerl の悪いところを集めてきたような...
うん,まぁ,やり過ぎには注意しろってこった.


気を取り直して,id:taedium さんの エントリ で紹介されている,node-seq のサンプル flowless 版いってみませう.

  • stat_all.js
var fs = require('fs');
var flowless = require('flowless');

var context = {};
flowless.runSeq([
  [fs.readdir, __dirname],
  flowless.bindFirst(context, 'fileNames'),
  flowless.map(function(file, cb) {
    fs.stat(__dirname + '/' + file, cb);
  }),
  flowless.array.reduce(function(sizes, stat, i) {
    sizes[context.fileNames[i]] = stat.size;
    return sizes;
  }, {})
], function(err, sizes) {
  if (err) throw err;
  console.dir(sizes);
});

node-seq に負けてる感があるけど,それはフロー制御とは関係なく,Hashish というユーティリティによる差だと思う.flowless.array.reduce() のところは非同期でも何でもないから.フロー制御モジュールだけで全てが解決するわけじゃないという当たり前の話ですね.それを考えると,Extra API って必要なのかどうか疑問かも.次のバージョンで整理するかも.かも.

  • parseq.js
var fs = require('fs');
var exec = require('child_process').exec;
var flowless = require('flowless');

flowless.runSeq([
  [exec, 'whoami'],
  flowless.par([
    [exec, 'groups', flowless.first],
    [fs.readFile, __filename, 'utf8']
  ])
], function(err, results) {
  if (err) throw err;
  console.log('Groups: ' + results[0][0].trim());
  console.log('This file has ' + results[1].length + ' bytes');
});

簡潔ではあるけど,最後の方で results[0][0] って出てくるのが微妙かなぁ.child_process.exec() って,コールバックに (err を除いて) 2 つ引数を渡すんですよね.stdout と stderr.やろうと思えば exec() の後に最初の引数だけ返すような関数を seq() でつないであげればいいだけなんだけど.

  • join.js
var flowless = require('flowless');

flowless.runPar([
  function(cb) {
    setTimeout(cb.bind(null, null, 'a'), 300);
  },
  function(cb) {
    setTimeout(cb.bind(null, null, 'b'), 200);
  },
  function(cb) {
    setTimeout(cb.bind(null, null, 'c'), 100);
  }
], function(err, results) {
  if (err) throw err;
  console.dir(results);
});

これは nue にかないませんね.bind() なんて使っているというのに! 無理矢理コンパクトに書けば,

var flowless = require('flowless');

flowless.runPar([
  function(cb) { setTimeout(cb.bind(null, null, 'a'), 300) },
  function(cb) { setTimeout(cb.bind(null, null, 'b'), 200) },
  function(cb) { setTimeout(cb.bind(null, null, 'c'), 100) }
], function(err, results) {
  if (err) throw err;
  console.dir(results);
});

って書けるけど... うん,今日の所は引き分けってことにしといちゃる.


比較はともあれ (JW),どれも十分簡潔に書けてはいるかなぁ.書けてはいるけど… 後で読んだ時に分かりやすいコードにはなってないかも.かも.かも.


ちなみに,現在の行数.

  85  229 2126 lib/extras.js
 148  390 3372 lib/flowless.js
  52  179 1389 lib/utils.js
 285  798 6887 合計

うわ,nue (執筆時点の nue.js は 262 行) よりでかくなってる... どこが less やねん.


次のバージョンでは nue v0.2.0 のマネをして,エラーハンドリングの強化やデバッグで役立つログ出力などをしたいなと思ってます.ますます more (肥大化) へ... 心より恥じる心より恥じる心より恥じる.