時代遅れひとりFizzBuzz祭り、今回はJavaScriptなのだけれども、今までとはちょっと違うアプローチでいこうと思う。というのも id:eel3:20100717:1279364463 のJScript編でECMA-262 第3版ベースのJavaScript的FizzBuzzを書いているので、同じようなことを繰り返しても面白くないからだ。第5版ネタで書こうにも、劇的に変化した訳でもないからなあ*1。
ではどう攻めるか? せっかくなので処理系ネタで行ってみようと考えたのだ。
今回は処理系としてRhino 1.7R3を使い、Rhinoの特長を生かしたFizzBuzzを目指してみた。前回のCASL IIとは「使用した処理系がJavaで書かれている」繋がりだ。ちなみに当初はSpiderMonkeyを候補として考えていたけど、最新版をビルドするのが面倒なので止めた。
RhinoはMozilla Foundationが保守・管理している由緒正しいJavaScriptの処理系だ。JavaScriptと強調するのは、所謂「JavaScript独自拡張」と言われる機能が使える処理系だからだ。世の中にJavaScriptの処理系と言われるものは幾つかあるものの、その大半はECMAScript互換であって(厳密な意味での)JavaScript互換ではない。保守している所がMozilla Foundationなだけあって、RhinoではJavaScript 1.7(Firefox 2相当)の機能まで使えるようだ。
もっともクライアントサイドでJavaScriptを書く場合は(DOMとかライブラリとかを除けば)ECMAScriptの範囲で書くことが大半だし、ECMA-262 第5版でも最近のメジャーなブラウザではあまり大きな問題にはならないと思う。Internet Explorer 6って何ですか?
そんな訳で、礼儀としてまずはECMAScriptの範囲だと思われるFizzBuzzを書いてみた。
/*jslint rhino: 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]; }; }()); print(iota(100, 1).map(fizzbuzz).join('\n')); }());
Rhino独自関数のprint()で結果を標準出力に書き出している部分を除けばECMAScriptの範囲に納まっているはず。
第5版になって配列操作用のメソッドが増えてうれしい。Rubyを知る身としては少々物足りないけど気にしない。
/*jslint rhino: 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]; }; }()); iota(100, 1).map(fizzbuzz).forEach(function (v) { print(v); }); }());
先ほどはjoin()で連結したので、今度はforEach()で舐めてみた。forEach()は昇順に要素を舐める。ECMA-262 第5版にそんな感じに書かれていた。
JavaScript独自拡張としてfor each-inという構文がある。通常のfor-inではオブジェクトのプロパティ名が列挙されるのに対して、for each-inではプロパティの値が列挙される。WikipediaのForeach文に関するページにはECMA-262 第5版の機能としてfor each-inが挙げられているが、第5版には含まれていないように見える。
/*jslint rhino: 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]; }; }()), v; for each (v in iota(100, 1).map(fizzbuzz)) { print(v); } }());
Array.forEach()からfor each-inに変えてみた。手元のRhinoでは偶然にも配列のインデックスの昇順で列挙されているが、列挙の順番は特に定められていないので要注意。
for each-in以外の列挙の手段としてIteratorがある。これもJavaScript独自拡張だ。
/*jslint rhino: true, plusplus: true, maxerr: 50, indent: 2 */ /*global Iterator: false */ (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]; }; }()), pair; for (pair in new Iterator(iota(100, 1).map(fizzbuzz), false)) { print(pair[1]); } }());
もっとも少し触った程度ではあまりメリットは感じなかった。
Iteratorにメリットを感じなかった原因の1つが「ジェネレータの存在」だ。個人的には「LuaのコルーチンのJavaScript版」という覚え方をしている。
/*jslint rhino: true, plusplus: true, maxerr: 50, indent: 2 */ (function () { 'use strict'; var MAX_COUNT = 100, fizzbuzz = function (count) { var ref = [null, 'Fizz', 'Buzz', 'FizzBuzz'], i, fizz, buzz; for (i = 1; i <= count; ++i) { ref[0] = i; fizz = i % 3 === 0 ? 1 : 0; buzz = i % 5 === 0 ? 2 : 0; yield(ref[fizz + buzz]); } }, fb = fizzbuzz(MAX_COUNT), i; for (i = 0; i < MAX_COUNT; ++i) { print(fb.next()); } }());
この例ではジェネレータを何回まで実行可能なのか呼び出し元で把握しているという前提の記述をしている。
実際にはジェネレータとして生成した関数が終了した後にジェネレータを呼び出すと例外StopIterationが発生するので、安直に考えれば例外をキャッチすることでジェネレータの実行可能回数を気にしなくてよくなる。
/*jslint rhino: true, plusplus: true, maxerr: 50, indent: 2 */ (function () { 'use strict'; var fizzbuzz = function (count) { var ref = [null, 'Fizz', 'Buzz', 'FizzBuzz'], i, fizz, buzz; for (i = 1; i <= count; ++i) { ref[0] = i; fizz = i % 3 === 0 ? 1 : 0; buzz = i % 5 === 0 ? 2 : 0; yield(ref[fizz + buzz]); } }, fb = fizzbuzz(100); for (;;) { try { print(fb.next()); } catch (e) { break; } } }());
とはいえ見栄えは良くない。
Cプログラマ的発想としてこんなコードを書きたくなる。
/*jslint rhino: true, plusplus: true, maxerr: 50, indent: 2 */ (function () { 'use strict'; var fizzbuzz = function (count) { var ref = [null, 'Fizz', 'Buzz', 'FizzBuzz'], i, fizz, buzz; for (i = 1; i <= count; ++i) { ref[0] = i; fizz = i % 3 === 0 ? 1 : 0; buzz = i % 5 === 0 ? 2 : 0; yield(ref[fizz + buzz]); } yield(null); }, fb = fizzbuzz(100), v; while ((v = fb.next()) !== null) { print(v); } }());
しかし実際にはこんな小細工は不要だ。for each-inを使えば済む。
/*jslint rhino: true, plusplus: true, maxerr: 50, indent: 2 */ (function () { 'use strict'; var fizzbuzz = function (count) { var ref = [null, 'Fizz', 'Buzz', 'FizzBuzz'], i, fizz, buzz; for (i = 1; i <= count; ++i) { ref[0] = i; fizz = i % 3 === 0 ? 1 : 0; buzz = i % 5 === 0 ? 2 : 0; yield(ref[fizz + buzz]); } }, v; for each (v in fizzbuzz(100)) { print(v); } }());
for each-in自体は要素の列挙順について何の保証も無いが、ジェネレータ側が意図した順に値を返すので問題はないはず。
このジェネレータと似たような挙動をクロージャで再現するとこうなる。
/*jslint rhino: true, plusplus: true, maxerr: 50, indent: 2 */ (function () { 'use strict'; var fizzbuzz = function (count) { var ref = [null, 'Fizz', 'Buzz', 'FizzBuzz'], i = 1; return { next: function () { var fizz, buzz; if (i > count) { throw { name: 'StopIteration', message: 'stop iteration' }; } ref[0] = i; fizz = i % 3 === 0 ? 1 : 0; buzz = i % 5 === 0 ? 2 : 0; ++i; return ref[fizz + buzz]; } }; }, fb = fizzbuzz(100); for (;;) { try { print(fb.next()); } catch (e) { break; } } }());
随分と面倒な実装になった。しかもfor each-inでの列挙には対応していないはず。
ジェネレータと組み合わせて使える機能として配列の内包がある。
/*jslint rhino: true, plusplus: true, maxerr: 50, indent: 2 */ (function () { 'use strict'; var fizzbuzz = function (count) { var ref = [null, 'Fizz', 'Buzz', 'FizzBuzz'], i, fizz, buzz; for (i = 1; i <= count; ++i) { ref[0] = i; fizz = i % 3 === 0 ? 1 : 0; buzz = i % 5 === 0 ? 2 : 0; yield(ref[fizz + buzz]); } }; print([v for each (v in fizzbuzz(100))].join('\n')); }());
配列の内包も、個人的にジェネレータと並んで「他の言語の機能」という印象が強い。ただ肝心のその言語が何か思い出せない……。
ここまではJavaScriptの独自機能を中心に見てきたので、Rhino独自の機能にも注目してみたい。おそらく最も特徴的なのはJavaの機能を呼び出すことが可能な点だ。
/*jslint rhino: true, plusplus: true, maxerr: 50, indent: 2 */ /*global Packages: false */ (function () { 'use strict'; var fizzbuzz = function (count) { var JavaString = Packages.java.lang.String, ref = [null, 'Fizz', 'Buzz', 'FizzBuzz'], retval = new Packages.java.util.ArrayList(count), i, fizz, buzz; for (i = 1; i <= count; ++i) { ref[0] = String(i); fizz = i % 3 === 0 ? 1 : 0; buzz = i % 5 === 0 ? 2 : 0; retval.add(new JavaString(ref[fizz + buzz])); } return retval; }, out = Packages.java.lang.System.out, fb = fizzbuzz(100), i, len; for (i = 0, len = fb.size(); i < len; ++i) { out.println(fb.get(i)); } }());
Javaの文字列とコレクション(ArrayList)を使い、System.out.printlnで出力してみた。
あと個人的に注目すべきだと思うのがモジュール化の実現方法。例えば以下のような内容のファイルがあったとする。
/*jslint rhino: true, plusplus: true, maxerr: 50, indent: 2 */ /*global Packages: false */ var fizzbuzz = function (count) { 'use strict'; var ref = [null, 'Fizz', 'Buzz', 'FizzBuzz'], retval = [], i, fizz, buzz; for (i = 1; i <= count; ++i) { ref[0] = i; fizz = i % 3 === 0 ? 1 : 0; buzz = i % 5 === 0 ? 2 : 0; retval.push(ref[fizz + buzz]); } return retval; };
これがfizzbuzz.jsというファイル名だったとすると、別のファイルにてこんな風に使うことができる。
/*jslint rhino: true, plusplus: true, maxerr: 50, indent: 2 */ /*global Packages: false, fizzbuzz: false */ (function () { 'use strict'; load('fizzbuzz.js'); var fb = fizzbuzz(100), i, len; for (i = 0, len = fb.length; i < len; ++i) { print(fb[i]); } }());
load()で読み込んでいる。多分、Node.jsなどのモジュール機能よりもクライアントサイドJavaScriptにてscriptタグでライブラリを読み込むアレに近いのではないかと思うが、自信がない。
一般的に、独自拡張機能には抗いがたい魅力をもつものが多いように思う。今回は使わなかったけど、例えばletなんて無茶苦茶使いたい。使いたいが、一度使ったら戻ってこれなくなりそうなので怖くて使えない。悩ましいところだ。
正直なところJavaScriptはひどい言語で、個人的にはひどさの方向性は違うもののC言語並みにひどい言語だと思う。この「ひどさ」はC++的な「自分の手に負えない」的なひどさ*2ではなく、C言語的な「背景を理解したうえで書かないと痛い目に遭う」という類だと思う。ある意味「書き手を選ぶ言語」で、それ故に『JavaScriptパターン ―優れたアプリケーションのための作法』のような本が必要とされる。
でも面白みのある実用言語で、好きなのだ。私個人としてはWSHでWindows用の小ツールを書くならVBScriptではなくJScriptを選択するだろう。ひどい部分は色々あるけどパワフルだ。関数が第一級のオブジェクトで、無名関数が使えて、クロージャが使える。関数を引数に取り関数を返す関数を書ける*3。最近は徐々に状況が変わりつつあるものの、実際に仕事で沢山の人が使っている言語でこういうことが簡単にできるものはそう多くはない。何しろWebアプリのクライアントサイドはJavaScript一択だからなあ、今のところ。
好きな言語は贔屓したくなるのが人情で、個人的にはクライアントサイド以外にもJavaScriptが普及してくれたら嬉しい。WSHは正直落ち目なので期待はしないようにしているけど*4、今回取り上げたRhinoを含めてブラウザ以外の処理系が意外とある訳で、何か1つでも流行ってくれないだろうかと思う。サーバサイドもいいけど、もっとこう、何というか「PerlやRubyでちょっとしたツールを書く」的な方向の選択肢の1つになってくれないだろうか。
*1:地味に進化しているとは思うけど……。
*2:暴論だ……。C++はその仕様の大きさ・複雑さが批判の対象となることが多いとおもうけど、それはつまるところ「自分の手に負えない」ということだと思う。仕様が大きい為に自分が理解できていない機能があり、その機能を使った他人のソースを理解できない――なんて所ではないだろうか。逆に言えばその壁を越えられれば大きなパワーを得られる可能性があるように思う。
*3:というか実際に仕事で書いた。
*4:万が一にもWSH + JScriptの組み合わせが爆発的に広まったとしたら、それはそれで微妙な所ではある。というのもWSH本体のシステムの抽象化自体は好きになれないからだ。過度の抽象化で使い勝手が悪くなっている気がしてならない。