node-flowless 0.0.0 リリース

しました.

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


node-flowless は Node.js 用の小さな (less) フロー制御 (control-flow) モジュールです.名前は flawless に引っかけたのであって typo じゃないんだからねっ!


id:taedium さんが自作のフロー制御モジュール nue に関するエントリを書いているのに刺激されて,自分もブログネタとして書いてみました.nue はたぶん,Step にインスパイアされたのだと思われますが,flowless は Slide に強く強くインスパイアされてます.Slide のコンセプトは以下のスライド (これが名前の由来w) が分かりやすいです.

Node の非同期 API には EventEmitter を使ったものと,コールバック関数を使ったもの,2 種類あるよーなんて時々書いてたりしますが,それはこのスライドからの受け売りです.


コールバックスタイルには以下のお約束があります.

  • 非同期関数は最後の引数としてコールバック関数を受け取る.
    • function(... cb)
  • コールバック関数は最初の引数としてエラーを受け取る.
    • function(err, ...)

フロー制御モジュールの役割は結局の所,この形式に従った非同期関数をつなぐためのコールバック関数を提供してあげることだけです.
バリエーションとしても複数の非同期関数を順番に実行するか,並行に実行するか,それだけで大抵は十分だったりします.Async でいうところの series と parallel ですね.あとは map() なんかの非同期版も重宝ではありますが,そういうのは Async 使えばいいかなー? もしかしたら flowless に入れるかもだけど.


ともあれ (JW),flowless では

  • flowless.series(functions, cb)
  • flowless.parallel(functions, cb)

という関数を提供しています.functions は関数の配列で,series() は一つずつ逐次的に,parallel() は全部まとめて並行的に呼び出します.


series() の場合,エラー無しでコールバック関数が呼ばれると,その第 2 引数以降 + コールバック関数を引数として次の非同期関数を呼び出します.全ての非同期関数が実行されると,最後に cb が呼び出されます.
コールバック関数にエラーが渡されたり,非同期関数が例外をスローすると残りの非同期関数は呼び出されることなく,cb が呼び出されます.

parallel() の場合,全ての非同期関数からエラー無しでコールバック関数が呼ばれると,その第 2 引数 (または第 2 引数以降の配列) を要素とする配列を引数として cb が呼び出されます.
コールバック関数にエラーが渡されたり,非同期関数が例外をスローすると,即座に cb が呼び出されます.


series()parallel(),エラーがあってもなくても,どの場合でも cb が呼び出されるのは一度だけです.


非同期関数の呼び出しを trycatch で囲っているのは特徴と言えるかもしれません.Slide も Async も囲んでないんですよね.Slide の作者 Isaacs は例外なんぞスローするなという主張なのですが,実際は Node のコア API でも引数のエラーチェックなどで例外をスローするわけで,これは必要じゃないかなと思ってます.
ちなみに Step は trycatch で囲ってくれるのですが,例外がスローされてもコールバックにエラーが渡されても,次の非同期関数が呼び出されるだけでエラー時のルーティングをしてくれないんですよね.trycatch とエラー時のルーティングをサポートしたシンプルなフロー制御モジュールってあまりない気がします.nue は両方やってくれるようです.


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

  • flowless.makeSeries(functions)
  • flowless.makeParallel(functions)

これらが返す関数は非同期関数なので,引数の最後にコールバックを付けて呼び出すことができます (その他の引数は makeSeries() の場合は最初の非同期関数,makeParallel() の場合は全ての非同期関数に引数として渡されます).実際の所,series()parallel() の実装は

function series(functions, cb) {
  makeSeries(functions)(cb);
}

function parallel(functions, cb) {
  makeParallel(functions)(cb);
}

ってだけだったりします.(^^;


そして flowless が Slide から一番影響を受けているのが,非同期 API を呼び出すためだけに一行だけの無名関数を書く手間を減らせることです.
つまり,

flowless.series([
  function(cb) {
    fs.readFile('path1', 'utf8', cb);
  },
  function(cb) {
    fs.readFile('path2', 'utf8', cb);
  },
  ...

なんて書く代わりに,

flowless.series([
  [fs.readFile, 'path1', 'utf8'],
  [fs.readFile, 'path2', 'utf8'],
  ...

って書くことができます.可読性? なにそれおいしいの?
っていうわけでもありませんが,一行野郎がだらだら続くコードの可読性が優れているとも思ってないので,これがいいのです.


んで,asyncblock のサンプルを flowless で書くとこうなります (taedium さんの nue 版は こちら).

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

flowless.series([
  flowless.makeParallel([
    [fs.readFile, 'path1', 'utf8'],
    [fs.readFile, 'path2', 'utf8']
  ]),
  function(results, cb) {
    fs.writeFile('path3', result.join(''), cb);
  },
  [fs.readFile, 'path3', 'utf8']
], function(err, result) {
  if (err) throw err;
  console.log(result);
  console.log('all done');
});

一行野郎が一人残ってますが,引数を加工したりする場合はしょうがないかなーってことで.ほぼ同等の記述をしている Async の例 (まじめに無名関数を書いている) と比べて,可読性は劣るどころか勝ってると言えるんじゃないかな?


一行野郎の配列表現では,直前のコールバックに渡された引数 (エラーを除く) を参照することもできます.

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

flowless.series([
  [fs.readFile, 'path1', 'utf8'],
  [fs.writeFile, 'path2', flowless.first, 'utf8']
], function(err, result) {
  if (err) throw err;
});

この例では,fs.writeFile() の 第 2 引数として flowless.first を指定しています.これにより,直前の非同期関数 fs.readFile() がコールバック function(err, data) に渡した (エラーを除いた後の) 最初の引数 datafs.writeFile() に渡されます.
つまり,上記は以下と等価です.

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

flowless.series([
  function(cb) {
    fs.readFile('path1', 'utf8', cb);
  },
  function(data, cb) {
    fs.writeFile('path2', data, 'utf8', cb);
  }
], function(err, result) {
  if (err) throw err;
});

ちなみに,secondthird もあります.それ以上必要なら無名関数を書いた方がいいでしょう.sixth とかじゃそれが何か分からないから.


ということで,flowless が現時点で提供している機能はこれだけです.
Less is more.Less but better.
そんなわけで (どんなわけで?),みんなも自分のフロー制御モジュールを書くといいと思うよ!!




id:taedium さんの反応を見て追記.

Map のたぐいは Async が充実しているのでそれを使えばいいかと思ってたのですが,せっかくなので試しに実装してみました.ほとんど parallel() のこぴぺですが.
まだコミットもプッシュもしていませんが,上記エントリと同じ例はこうなりました.

'use strict';

var fs = require('fs');
var flowless = require('../index');

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

しれっと makeAsync() なんて増えてますが,これは同期的な (値を返す) 関数を非同期関数の皮で包んであげるだけのものです.つまり,戻り値やスローされた例外をコールバックに渡してくれます.


どうしようかなー,含めてもいいかなー.でもあまり増やすと less じゃなくて more になっちゃうしなー.実践で使ってないから判断材料に乏しい...