時代遅れひとりFizzBuzz祭り、今回もJavaScript de 処理系ネタ。最近流行のNode.jsを触ってみた。サーバサイド! 前回のRhinoはその血筋的に微妙にクライアントサイドの流れを汲んでいるように思う*1けど、それとは対照的だ*2。
個人的に、サーバサイドJavaScriptが成功するか否かは別として、Node.jsには一処理系として広まって欲しいと思っている。何しろJavaScriptはWindows Script Host以外にローカルでテキスト処理したりする時に使えるメジャーな処理系が少ない。PerlやRubyで使い捨てツールを書くようなシチュエーションでNode.jsが使われるようになると結構面白いと思うのだ。
今回はv0.6.15のWindows版バイナリを使用した。一部v0.7.8も使っている。
普通のFizzBuzz
普通にFizzBuzzすると、例えばこんな感じだろうか?
/*jslint devel: true, plusplus: true, maxerr: 50, indent: 2 */ (function () { 'use strict'; var iota = function (count, start, step) { var retval = [], i; if (start === undefined) { start = 0; } if (step === undefined) { step = 1; } for (i = 0; i < count; ++i) { retval[i] = start + step * i; } return retval; }, fizzbuzz = (function () { var ref = [null, 'Fizz', 'Buzz', 'FizzBuzz']; return function (n) { var fizz = n % 3 === 0 ? 1 : 0, buzz = n % 5 === 0 ? 2 : 0; ref[0] = n; return ref[fizz + buzz]; }; }()); console.log(iota(100, 1).map(fizzbuzz).join('\n')); }());
標準出力、標準エラー出力を使う為にconsoleモジュールが用意されている。見た目はクライアントサイドJavaScriptでconsoleオブジェクトを使うのと変わらない。
モジュール機能
Node.jsはサーバサイドの処理系なので、モジュール化の手法はクライアントサイドとは異なる。
/*jslint node: true, plusplus: true, maxerr: 50, indent: 2 */ (function () { 'use strict'; var iota = function (count, start, step) { var retval = [], i; if (start === undefined) { start = 0; } if (step === undefined) { step = 1; } for (i = 0; i < count; ++i) { retval[i] = start + step * i; } return retval; }, fizzbuzz = (function () { var ref = [null, 'Fizz', 'Buzz', 'FizzBuzz']; return function (n) { var fizz = n % 3 === 0 ? 1 : 0, buzz = n % 5 === 0 ? 2 : 0; ref[0] = n; return ref[fizz + buzz]; }; }()); exports.answers = function (count) { if (typeof count !== 'number') { throw new TypeError('fizzbuzz.answers(): count must be number'); } if (count <= 0) { throw new RangeError('fizzbuzz.answers(): count must be > 0'); } return iota(count, 1).map(fizzbuzz); }; }());
`exports.answers' の辺りがモジュールの公開部。全体を無名関数で囲っているけど、どうもNode.jsでは関数の外で変数を定義してもモジュール(というかファイル?)の外には公開されないらしい。
このモジュールが fizzbuzz.js というファイルで定義されていたとすると、その使い方は例えばこんな感じ。
/*jslint node: true, maxerr: 50, indent: 2 */ (function () { 'use strict'; var fizzbuzz = require('./fizzbuzz.js'); fizzbuzz.answers(100).forEach(function (v) { console.log(v); }); }());
一旦変数fizzbuzzで受けているが、この辺りは好き好きだろう。
readline
個人的には単なる処理系としてのNode.jsにも期待する所がある。
例えばNode.jsでの入力ストリームの扱いは「適当な大きさのチャンクが非同期にコールバック関数の引数として渡される」というパターンが多いのだけど、何らかのテキストデータを処理したい場合は入力が「適当な大きさのチャンク」よりも「1行」である方が都合がよいことも多い*3。
コンソールというかttyからのユーザ入力に関しては、readlineモジュールを使うことで手軽に1行ごとにデータを扱うことができる。
/*jslint node: true, maxerr: 50, indent: 2 */ (function () { 'use strict'; var readline = require('readline'), rl = readline.createInterface(process.stdin, process.stdout), fizzbuzz = (function () { var ref = [null, 'Fizz', 'Buzz', 'FizzBuzz']; return function (n) { var fizz = n % 3 === 0 ? 1 : 0, buzz = n % 5 === 0 ? 2 : 0; ref[0] = n; return ref[fizz + buzz]; }; }()); rl.on('line', function (line) { var count = line.trim(); if (count === '') { // EMPTY } else if (/^[1-9][0-9]{0,8}$/.test(count)) { console.log(fizzbuzz(count)); } else { console.error("Invalid input: '%s'", line); } }).on('close', function () { process.exit(0); }).resume(); }());
標準入力からのユーザ入力に対してFizzBuzzの答えを返す実装の例。C-cないしC-dで終了する。
この実装はユーザが手入力する分には問題ないが、例えばseqで生成した改行区切りの数列をパイプ経由で流し込んだり、又は数列を記述したテキストをリダイレクトで流し込んだりしようとすると、v0.6.15ではこんな実行時エラーが発生する。
node.js:201 throw e; // process.nextTick error, or 'error' event on first tick ^ AssertionError: stdin must be initialized before calling setRawMode
v0.7.8では実行時エラーは発生しないのだが、何故か入力値自体もコンソールにエコーバックされてしまう。直感的には入力自体はエコーバックされずにFizzBuzzの答えの部分だけが出力されるように思うのだが……。
この傾向は、例えばfsモジュールを使ってファイルからデータを読み出すようにしても変わらない。
/*jslint node: true, maxerr: 50, indent: 2 */ (function () { 'use strict'; var fs = require('fs'), readline = require('readline'), fizzbuzz = (function () { var ref = [null, 'Fizz', 'Buzz', 'FizzBuzz']; return function (n) { var fizz = n % 3 === 0 ? 1 : 0, buzz = n % 5 === 0 ? 2 : 0; ref[0] = n; return ref[fizz + buzz]; }; }()), input, rl; input = fs.createReadStream('./fizzbuzz5.txt', { encoding: 'utf8' }).on('error', function (e) { console.error('error: [%s] %s', e.name, e.message); process.exit(1); }); input.pause(); rl = readline.createInterface(input, process.stdout); rl.on('line', function (line) { var count = line.trim(); if (count === '') { // EMPTY } else if (/^[1-9][0-9]{0,8}$/.test(count)) { console.log(fizzbuzz(count)); } else { console.error("Invalid input: '%s'", line); } }).on('close', function () { process.exit(0); }).resume(); }());
これの挙動もv0.6.15、v0.7.8共に前のバージョンと同じになる。
やはりreadlineはttyの入出力専用、ということだろうか? もう少し汎用的に入力を行単位に扱える仕組みが欲しい。
HTTPクライアント/サーバ(GET)
サーバサイドJavaScriptの処理系ということもあり、HTTPのクライアント/サーバとして振舞うアプリを書くのは比較的簡単だ。
例えば指定した数だけFizzBuzzの答えをサーバから取得するHTTPクライアントはこんな風に書くことができる。
/*jslint node: true, maxerr: 50, indent: 2 */ (function () { 'use strict'; var options = { host: 'localhost', port: 5432, path: '/fizzbuzz?count=100' }; require('http').get(options, function (response) { if (response.statusCode === 200) { response.pipe(process.stdout); response.resume(); } else { console.error('status: %d', response.statusCode); } }).on('error', function (e) { console.error('error: [%s] %s', e.name, e.message); }); }());
GETメソッドを使用し、URLにパラメータを付加している。今回はパラメータを直書きしているけど、必要に応じてquerystring.stringifyなどを使うべきだろう。サーバからのレスポンスはそのまま標準出力に垂れ流すようにしている。
しかしまあ使い捨てHTTPクライアントをさっと書く程度なら他のスクリプト言語でも問題ない。標準ライブラリにHTTPクライアント・ライブラリの類が用意されているものだ。
Node.jsが変わっている(失礼)のは、サーバサイドJavaScriptの処理系なだけにHTTPサーバ的な振る舞いをするプログラムを書く為のモジュールが標準で用意されていることだ。
/*jslint node: true, plusplus: true, maxerr: 50, indent: 2 */ (function () { 'use strict'; var url = require('url'), hasOwn = Object.prototype.hasOwnProperty, have = function (hash, key) { return hasOwn.call(hash, key); }, iota = function (count, start, step) { var retval = [], i; if (start === undefined) { start = 0; } if (step === undefined) { step = 1; } for (i = 0; i < count; ++i) { retval[i] = start + step * i; } return retval; }, fizzbuzz = (function () { var ref = [null, 'Fizz', 'Buzz', 'FizzBuzz']; return function (n) { var fizz = n % 3 === 0 ? 1 : 0, buzz = n % 5 === 0 ? 2 : 0; ref[0] = n; return ref[fizz + buzz]; }; }()); require('http').createServer(function (request, response) { var requrl, query, count; if (request.method !== 'GET') { response.statusCode = 405; response.end(); return; } response.statusCode = 400; requrl = url.parse(request.url, true); if (!have(requrl, 'pathname') || (requrl.pathname !== '/fizzbuzz')) { response.end(); return; } if (!have(requrl, 'query')) { response.end(); return; } query = requrl.query; if (!have(query, 'count') || !/^[1-9][0-9]{0,8}$/.test(query.count)) { response.end(); return; } count = Number(query.count); console.assert(isFinite(count)); response.writeHead(200, {'Content-Type': 'text/plain'}); response.end(iota(count, 1).map(fizzbuzz).join('\n') + '\n'); }).on('error', function (e) { console.error('error: [%s] %s', e.name, e.message); }).listen(5432); }());
先ほどのクライアントに対するサーバ側の実装例。HTTPとしてプリミティブな機能ばかりなので本格的なWebアプリを書くには向かない*4ものの、ダミーのCGIもどきを標準の機能のみで比較的手軽に書ける点はすごいと思う。
HTTPクライアント/サーバ(POST)
HTTPクライアントがGETメソッドを使うパターンは多いのでhttp.getという専用のメソッドが用意されている。一方、GET以外のメソッドを使いたい場合はhttp.requestという汎用のメソッドを使用し、オプションで使用したいメソッドを指定することになる。
/*jslint node: true, maxerr: 50, indent: 2 */ (function () { 'use strict'; var options = { method: 'POST', host: 'localhost', port: 5432, path: '/fizzbuzz.cgi' }; require('http').request(options, function (response) { if (response.statusCode === 200) { response.pipe(process.stdout); response.resume(); } else { console.error('status: %d', response.statusCode); } }).on('error', function (e) { console.error('error: [%s] %s', e.name, e.message); }).end('count=100'); }());
POSTを使用し、先ほどはURLに付加していたパラメータをリクエストボディで送信するようにしてみた。
リクエストにしろレスポンスにしろ、メッセージボディを受け取る際には注意が必要だ。というのもイベント `data' の引数として引き渡されるデータは「メッセージボディの断片」なので、全データを一度に or 1行ごとに受け取れる保証はない。
/*jslint node: true, plusplus: true, maxerr: 50, indent: 2 */ (function () { 'use strict'; var querystring = require('querystring'), hasOwn = Object.prototype.hasOwnProperty, have = function (hash, key) { return hasOwn.call(hash, key); }, iota = function (count, start, step) { var retval = [], i; if (start === undefined) { start = 0; } if (step === undefined) { step = 1; } for (i = 0; i < count; ++i) { retval[i] = start + step * i; } return retval; }, fizzbuzz = (function () { var ref = [null, 'Fizz', 'Buzz', 'FizzBuzz']; return function (n) { var fizz = n % 3 === 0 ? 1 : 0, buzz = n % 5 === 0 ? 2 : 0; ref[0] = n; return ref[fizz + buzz]; }; }()); require('http').createServer(function (request, response) { var body; if (request.method !== 'POST') { response.statusCode = 405; response.end(); return; } if (request.url !== '/fizzbuzz.cgi') { response.statusCode = 404; response.end(); return; } request.setEncoding('utf8'); body = []; request.on('data', function (chunk) { body.push(chunk); }); request.on('end', function () { var query = querystring.parse(body.join('')), count; if (!have(query, 'count') || !/^[1-9][0-9]{0,8}$/.test(query.count)) { response.statusCode = 400; response.end(); return; } count = Number(query.count); console.assert(isFinite(count)); response.writeHead(200, {'Content-Type': 'text/plain'}); response.end(iota(count, 1).map(fizzbuzz).join('\n') + '\n'); }); }).on('error', function (e) { console.error('error: [%s] %s', e.name, e.message); }).listen(5432); }());
POST版クライアントに対応するサーバ側の実装例。リクエストボディを受信する度にチャンクを配列に詰めていき、受信終了時に連結してから解析を始めるようにしている。
これを書いていて面白いと思ったのは、クロージャを使うことでリクエスト単位で各オブジェクトをまとめてしまえる所だ。この例では変数bodyはリクエストごとに保持する必要があるのだが、クロージャによってほとんど意識することなくそれを実現できている。
Node.jsのHTTP回りの機能は結構充実している。httpモジュールとほぼ同じインターフェースを持つhttpsモジュールが標準で付いているし、サードパーティのモジュールにはWebSocketやWebフレームワークを提供するものがある。それらを使わなくても簡単な使い捨てツールの類ならHTTPクライアント、サーバのどちら側も結構手軽に書ける。特にサーバ的な使い捨てアプリを手軽に書けて且つWebサーバが不要な(Node.js単体で動作する)事は、個人的にはポイントが高い。
TCPクライアント/サーバ
HTTPより下層のプロトコルも使える。例えばTCPによるクライアントはこんな感じ。
/*jslint node: true, maxerr: 50, indent: 2 */ (function () { 'use strict'; var socket; socket = require('net').createConnection(5432, 'localhost'); socket.on('connect', function () { socket.setTimeout(10000); socket.end('100'); socket.pipe(process.stdout); }).on('timeout', function () { console.error('error: connection timeout'); socket.destroy(); }).on('error', function (e) { console.error('error: [%s] %s', e.name, e.message); socket.destroy(); }); }());
C言語でソケットプログラミングしたことがある身としては、TCPソケットっぽい部分が色々とあるのにJavaScript的な無名関数によるコールバックを多用したスタイルになっているので不思議な感じだ。
今度はサーバ側。クライアント側もそうだったがHTTPバージョンと似たような書き方になっている。
/*jslint node: true, plusplus: true, maxerr: 50, indent: 2 */ (function () { 'use strict'; var util = require('util'), iota = function (count, start, step) { var retval = [], i; if (start === undefined) { start = 0; } if (step === undefined) { step = 1; } for (i = 0; i < count; ++i) { retval[i] = start + step * i; } return retval; }, fizzbuzz = (function () { var ref = [null, 'Fizz', 'Buzz', 'FizzBuzz']; return function (n) { var fizz = n % 3 === 0 ? 1 : 0, buzz = n % 5 === 0 ? 2 : 0; ref[0] = n; return ref[fizz + buzz]; }; }()); require('net').createServer({ allowHalfOpen: true }, function (socket) { var body = []; socket.setEncoding('utf8'); socket.setTimeout(30000); socket.on('data', function (chunk) { body.push(chunk); }).on('timeout', function () { socket.destroy(); }).on('error', function (e) { socket.destroy(); }).on('end', function () { var input = body.join(''), count = input.trim(); if (/^[1-9][0-9]{0,8}$/.test(count)) { count = Number(count); console.assert(isFinite(count)); socket.end(iota(count, 1).map(fizzbuzz).join('\n') + '\n'); } else { socket.end(util.format("Invalid input: '%s'\n", input)); } }); }).on('error', function (e) { console.error('error: [%s] %s', e.name, e.message); }).listen(5432, 'localhost'); }());
HTTPが使えるので直接TCPで送受信する機会は少ないと思うが、基本的にストリームな所はHTTPと同じなので強い違和感を感じることなく実装できるのではないだろうか?
UDPクライアント/サーバ
UDPも使えるけど、実際に使う機会は少ないと思う。例えばパケットの到着順は保証されていないし、途中で消失してしまう可能性もある。TCPは内部で色々と頑張っているけどUDPではユーザ(プログラマ)の責任になるので、それなりに品質を確保しようとすると実装が大変になる。
/*jslint node: true, plusplus: true, maxerr: 50, indent: 2 */ (function () { 'use strict'; var QU = new Buffer('QU'), TIME_OUT = 10000, iota = function (count, start, step) { var retval = [], i; if (start === undefined) { start = 0; } if (step === undefined) { step = 1; } for (i = 0; i < count; ++i) { retval[i] = start + step * i; } return retval; }, socket, query = function (n) { var msg = new Buffer(QU.length + 4); QU.copy(msg); msg.writeUInt32BE(n, QU.length); socket.send(msg, 0, msg.length, 5432, 'localhost'); }, timeout = function () { console.error('error: timeout'); socket.close(); }, index, timerId; socket = require('dgram').createSocket('udp4'); socket.on('message', function (msg, rinfo) { var status, len; if (msg.length < 2) { console.error('?'); return; } status = msg.toString('utf8', 0, 2); if (status !== 'OK') { console.error('%s', status); return; } if (msg.length < 4) { console.error('error: packet NG'); return; } len = msg.readUInt8(2); if ((3 + len) > msg.length) { console.error('error: packet NG'); return; } console.log('%s', msg.toString('utf8', 3)); ++index; clearTimeout(timerId); if (index <= 100) { timerId = setTimeout(timeout, TIME_OUT); query(index); } else { socket.close(); } }).on('error', function (e) { console.error('error: [%s] %s', e.name, e.message); socket.close(); }); index = 1; timerId = setTimeout(timeout, TIME_OUT); query(index); }());
UDP版クライアントの実装例。幾つかの要因が重なった為にTCP版よりもコードが長く複雑になっている。この実装ではパケットが1つでも消失すると処理が終了してしまうのが難点だろうか?
サーバ版の実装も、TCP版よりも役割が減った割にはコードが長めだと思う。
/*jslint node: true, maxerr: 50, indent: 2 */ (function () { 'use strict'; var OK = new Buffer('OK'), NG = new Buffer('NG'), fizzbuzz = (function () { var ref = [null, 'Fizz', 'Buzz', 'FizzBuzz']; return function (n) { var fizz = n % 3 === 0 ? 1 : 0, buzz = n % 5 === 0 ? 2 : 0; ref[0] = n.toString(); return ref[fizz + buzz]; }; }()), socket; socket = require('dgram').createSocket('udp4'); socket.on('message', function (msg, rinfo) { var reply = function (msg) { socket.send(msg, 0, msg.length, rinfo.port, rinfo.address); }, num, fb, body; if (msg.length !== 6) { reply(NG); return; } if (msg.toString('utf8', 0, 2) !== 'QU') { reply(NG); return; } num = msg.readUInt32BE(2); if (num === 0) { reply(NG); return; } fb = new Buffer(fizzbuzz(num)); body = new Buffer(OK.length + 1 + fb.length); OK.copy(body); body.writeUInt8(fb.length, OK.length); fb.copy(body, OK.length + 1); reply(body); }).on('error', function (e) { console.error('error: [%s] %s', e.name, e.message); }).bind(5432, 'localhost'); }());
バイナリデータの扱いがどうにも冗長気味に感じる。
実際の所、UDPを選択するのは次の条件に当てはまるケースが多いと思う。
- TCPの挙動、特にパケット消失時の再送が問題になる場合。
- パケットが多少消失しても大丈夫か、又は再送以外の方法でカバーできる場合。
まとめ
Node.jsは興味深い。何度も書いているけど、サーバサイドJavaScript云々を抜きにしてもクライアントサイドJavaScript以外の実用的なスクリプト処理系として価値があると思う。言語がJavaScriptだしコールバックを多用する独特のスタイルだし、PerlやRubyなどと比較すれば癖が強い気がする。しかし何というか癖の強さ故にハマる人はハマるとでもいうか、「Cの皮を被ったLisp」という異名は伊達ではないというか……。
私個人としては「WSHでツールを作るならVBScriptよりもJScriptを選ぶ」という程度にはJavaScriptが好きな訳で、Windows以外の環境でもそういった小ツール作成用に使える処理系が広まってくれるのは悪い話ではない。
とはいえ普及するにはキラーアプリというか何かしらの目玉が必要な訳で、現状ではそれが「WebSocketでサーバサイドJavaScript」なのだと思うけど、正直なところどうなのだろう? 既存のサーバサイド用言語なりライブラリなりがWebSocketに対応してしまったらオシマイな気がするのだが、門外漢なのでよく分からない。
*1:SpiderMonkeyの組み込み先が組み込み先なだけに。しかし当然ながらRhino自体はクライアントサイドJavaScriptの環境ではない。
*2:――そう思っているのは私だけ?
*3:それは処理の都合かもしれないし、実装する側の慣れの問題かもしれない。