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 (肥大化) へ... 心より恥じる心より恥じる心より恥じる.