このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

[参照用 記事]

JavaScript用のアサーションを作ってみる

ここで触れたシステム、ある程度は動くので、サンプルを作り始めました。すると、あれまー、随分とバグがあるなー。

これは全面的に僕の監督責任ですわ。そもそもが、

たぶん、こうだから、
おそらく、こうすればよくて、
うまくいけば、あーなるはずだぜ。

みたいな方針しか言ってないし、「防衛コードを書くな」といったアドバイスも説明不足でうまく伝わらなかったようです。

内容:

  1. 防衛コード、うざすぎ
  2. 防衛と契約(コントラクト)は違うんだよ
  3. アサーションを使えばこうなる
  4. 安直なアサーションを作ってみる
  5. ライブラリコードとビルドシステム

●防衛コード、うざすぎ

防衛コードに関して言えば、歴戦の勇士のような職業プログラマが「俺は誰も信じねー!」みたいなコードを書くわけですよ。


function sum(x, y) {
var objectUsed = false;
if (x === undefined) {
alert("第1引数がundefinedだってぇ、ひでーな、このボケ。");
throw new Error("ボケ 1");
}
if (x === null) {
alert("第1引数がnullだってぇ、どうかしてるぜ、このタコ。");
throw new Error("タコ 1");
}
if (typeof x == 'object' && x instanceof Number) {
alert("引数にobjectはやめろや、いちおう計算はしてやっけど。");
objectUsed = true;
x = x.valueOf();
}
if (typeof x != 'number') {
alert("グォラー、数じゃねー第1引数入れるな! バカ。");
throw new Error("バカ 1");
}
if (isNaN(x)) {
alert("NaNは計算してやんねーって、このハゲ。");
throw new Error("ハゲ 1");
}
// 第1引数のチェック終了

if (y === undefined) {
alert("第2引数がundefinedだってぇ、ひでーな、このボケ。");
throw new Error("ボケ 2");
}
if (y === null) {
alert("第2引数がnullだってぇ、どうかしてるぜ、このタコ。");
throw new Error("タコ 2");
}
if (typeof y == 'object' && y instanceof Number) {
if (objectUsed) {
alert("だーから、objectはやめろって言ったろ、"
+ "もうやめたぞ、アンポンタン。");
throw new Error("アンポンタン");
} else {
alert("引数にobjectはやめろや、いちおう計算はしてやっけど。");
y = y.valueOf();
}
}
if (typeof y != 'number') {
alert("この野郎、ここまで来てワケわかんねー第2引数ってのは、"
+ "どういうリョウケンだテメー、バカ。");
throw new Error("バカ 2");
}
if (isNaN(y)) {
alert("おしかったなー、NaNは計算してやんねーって、このハゲ。");
throw new Error("ハゲ 2");
}
// 第2引数のチェック終了

alert("よーし、まーいいだろう。");

return x + y;
}

みたいな…… 膨大な防衛コードに紛<まぎ>れて、「本質的には何をやっているか」が見えなくなってしまうのですね。この場合なら実は:


function sum(x, y) {
return x + y;
}
って書けばいいんですよ。引数が正当であることを保証するのは関数を呼ぶ側の責任です。

[追記]上の例はジョークなんで、かえって誤解のもとになるかもしれません。コメント欄に補足説明があるので、そちらも参照してください。[/追記]

●防衛と契約(コントラクト)は違うんだよ

とはいっても:

  1. どこの誰から呼ばれるかが前もって予測できない。
  2. 呼ぶ側が責任を果たしているという保証がない。

と、そういう事態はあります。1の場合は、防衛コードというよりは、当該関数なりメソッドの責務の一部として引数やその他の入力データのチェックが必要です。

問題は2のケースです。性善説、あるいは自分と同僚を信じる立場ならば「なにもしない」が正解です。それは極論だとしても*1、いつまでも残るようなチェックコードは感心しません。メイヤー先生の契約駆動プログラミングの発想に基づくべきでしょう。

契約をチェックするコードは、開発中は機能させて、最終的には取り外せるようにするわけです。EiffelやD言語では言語機能により契約をサポートしています。xUnitの方式では、契約チェックに相当するコード(ユニットテスト)を外に出して、本来のロジックを汚さないようにしています。

しかし、もとのソースコードに埋め込む方式もそんなに悪くはありません。昔のC言語のアサーションのような素朴は方式でもけっこう役に立ちます。それで、JavaScript向けの古典的/単純なアサーション(表明)を小一時間ででっちあげてみました。

●アサーションを使えばこうなる

先の過剰防衛的sumと同等に安全な関数は、アサーションを用いて次のように書けます。


function sum(x, y) {
ASSERT(isNormalNumber(x));
ASSERT(isNormalNumber(y));

return x + y;
}

isNormalNumberは次のような関数です(((x != null) はなくても大丈夫なようですが、この書き方がイディオム化しています。))。


function isNumber(x) {
return typeof x == 'number' || (x != null && x instanceof Number);
}

function isNormalNumber(x) {
return isNumber(x) && !isNaN(x);
}

各種データ型チェック関数は前もって準備してライブラリ化しておくとよいでしょう。

アサーション機能の要件として、とりあえず次の3つを要求しましょう。

  1. 不要になったら取り外せる
  2. 指定された条件式が偽のときは例外で強制終了する。
  3. ソースコードの場所(ファイル名、行番号)を表示できる。

●安直なアサーションを作ってみる

アサーションの1番目の要件「取り外し可能」は、テキスト処理で削り落としてしまえばいいでしょう(なにしろ安直だからな)。例えば:


> perl -i.asserted.js -n -p -e "s/ASSERT\s*\(//" sum.js

とすれば、文字列"ASSERT"を含む行が削除されます*2。この単純な方法が機能するためには、アサーションを複数行に渡って書くのは禁止ですね。


// これはダメ
function sum(x, y) {
ASSERT(isNormalNumber(x) &&
isNormalNumber(y)); // この行だけ残ってしまう

return x + y;
}

ASSERTに渡される条件式が偽のときに例外を投げるには、そういう関数を書けばいいのですけど、3番目の要件「ソースコードの場所を表示できる」ためにひと工夫いります。この点もテキスト処理と組み合わせることにして、ASSERTの実体となる関数をまず書いておきます。


/* assert.js */

function __assert__ (location, cond, msg) {
if (!msg) {
msg = "";
}
if (!cond) {
var s = "Assertion failed!\n" + location + " " + msg;
alert(s);
throw new Error(s);
}
}

/* ASSERTという文字列マッチに引っかからないためのトリック */
window["ASS" + "ERT"] = function (cond, msg) {
__assert__("", cond, msg);
}

[追記]トリックを追加しました。[/追記]

関数ASSERTも定義してあるので、このままでも使えます。が、ソースコードの場所は表示できません。オリジナルのソースファイルを次のようなスクリプトで処理します。


#! /bin/perl

#
# fixcode
#
for (@ARGV) {
my $file = $_;
next unless (open(FILE, $file));
while () {
s/ASSERT\s*\(/__assert__("$file:$.:",/g;
print;
}
close(FILE);
}


> perl fixcode.pl sum.js > sum.debug.js

すると、sum.debug.jsは次のようになります。


function sum(x, y) {
__assert__("sum.js:2:",isNormalNumber(x));
__assert__("sum.js:3:",isNormalNumber(y));

return x + y;
}

firebugやRhinoなどを使っていれば、例外を発生させたソースコードの場所は特定できるので、これは無駄な処理のように思えます。が、状況によりそうとも言えないのです。

●ライブラリコードとビルドシステム

JavaScriptのライブラリを構成する場合に、次のジレンマがあります。

  1. 開発中は、短い複数のソースファイルのほうが扱いやすい。
  2. 使うときは、ソースファイルが1つにまとまっていたほうが使いやすい。

我々の場合は、ビルドプロセス(makeを利用)の一部で次のような作業を行っています。


cat $(JS_SRC_LIST) > mylib.js

ここで$(JS_SRC_LIST)は、いくつかのJavaScriptファイルを並べたリスト、例えば、assert.js foo.js bar.js baz.js です。

mylib.jsがブラウザなどにロードされて実行されますが、mylib.jsの行番号が分かっても、もとのソース(foo.js, bar.js, baz.jsのどれか)の行番号は分かりません。そこで、単にcatする代わりに、


perl fixcode.pl $(JS_SRC_LIST) > mylib.js
のようにすれば、アサーションからもとのソースコード行番号を特定できます。

いずれにしても、ソースコードをテキスト処理で書き換えるという乱暴な手段を使っているので、ビルドシステムに組み込まないと煩雑でやってられないことになるでしょう。

こんな安直なアサーションがどこまで役に立つか? 今日思いついただけなのでわかりません。使ってみてまた報告します。

*1:チームの能力や状況によっては、意外とうまくいきます。

*2:もとのファイルは sum.js.asserted.js とリネームされます。.js拡張子が2つあって気持ち悪いですが、Dojo Toolkitでも、dojo.js.uncompressed.js なんて名前を使っていますよ。