node-flowless v0.0.2 リリース
しました.
npm install flowless
でインストールすることができます.
v0.0.1 からの主な変更点は次のとおりです.
- Bug
- core:
seq()
やpar()
のfunctions
や,map()
のarray
が空だった場合のバグを修正しました.
- core:
- Improvement
- core:
map()/runMap()
が呼び出す非同期関数の引数並びをArray.map()
に近づけました. - extras:
extras
名前空間に分離しました.
- core:
- 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 系を混ぜて扱えるようにしたら便利かなとか,ついつい肥大化路線で考えてしまう罠.