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 系を混ぜて扱えるようにしたら便利かなとか,ついつい肥大化路線で考えてしまう罠.