対話的JavaScript処理系(この記事を参照)におけるテスト/デバッグ手段として、ちょっと面白いことを考えたので紹介します。まー、ジャスト・ア・思い付きですけどね。
で、これって、かすかにアスペクト指向と言えなくもないかもしれない、とか思ったりして(って、まどろっこしい!)。
内容:
アドバイス機能とその用途
最近のAspectJとかってなんだか難しげなんですが、僕が最初にアスペクト指向の原始的な姿に触れたのは、Emacs Lispのアドバイス機能です。これは簡単なものです。特定の関数の実行の前(before)、後(after)、またはその関数を包み込む(around)形で、“アドバイス”(advice; なんで"advice"なのかは知らない)と呼ばれるコード断片の実行を付加できるのです。
JavaScriptでEmacs Lispのアラウンド・アドバイス(around advice)と似た機能を実現してみます。ただし、仕掛けはEmacsのものとは違います。割り込みハンドラをインターセプトするのと似た方法を使います。つまり、もとの関数をアドバイス関数で置き換えてしまうのですが、そのアドバイス関数内から、もとの関数を呼び出せるようにするのです。
Emacsのアドバイスは、もとの関数の挙動をわずかに変更したいときに使うのが主たる用途のようです。が、JavaScript版アドバイス機能は、対話的環境でのテスト/デバッグに便利そうな感じです。
仕組みと使い方(とソースコード)
addAdvice(funcFQN, advice) と removeAdvice(funcFQN) という2つの関数を準備しました。その機能と引数は次のようなものです:
- 関数 addAdvice:アラウンド・アドバイスを目的の関数に付加する。
- 引数 funcFQN:目的の関数を表すフルネーム(fully-qualified name)文字列。
- 引数 advice:アラウンド・アドバイスとなる関数。
- 関数 removeAdvice: アラウンド・アドバイス(があればそれ)を、目的の関数から削除する。
ここでフルネームとは、"MyObj.myfunc" のようにピリオドで区切られた名前の列ですが、関数が大域関数ならピリオドを含まない単なる名前です。
アドバイス関数(mylib.myTypeCheckerとmoved)が前もって定義されているとして、次のように使います。
addAdvice('myGlobalFunc', mylib.myTypeChecker); addAdvice("Point.prototype.moveTo", moved); removeAdvice('myGlobalFunc'); removeAdvice("Point.prototype.moveTo");
アドバイスにアドバイスを付けようとするとエラーになりますが、addAdvice の第3引数(force flag)をtrueにするとネストしたアドバイスも付けられます。それと、大域オブジェクトを名前で参照する必要があるので、変数([追記] nak2kさんの助言で、不要になった。)window
または_global
に大域オブジェクトをセットしておいてください。
なかでやっていることはこんなことです。
アドバイス関数のコピーを毎回作っている(実行時に再度コンパイルする)のは、それをしないとすぐさま循環呼び出しが発生してしまうからです。
アドバイス関数の書き方
アドバイス関数は次のお約束(コーディングパターン)に従って書きます。
function adviceFunc() { // オマジナイ var original = arguments.callee.original; var originalName = arguments.callee.originalName; // <strong>... ここに事前処理</strong> var retVal = original.apply(this, arguments);// もとの関数の呼び出し // <strong>... ここに事後処理</strong> // 戻り return retVal; }
アドバイスからもとの関数を全然呼び出さなくても、複数回呼び出してもかまいません。例えば:
fuction alwaysTrue() { return true; } addAdvice('my.complicatedPredicate', alwaysTrue);
とすると、(一時的に) my.complicatedPredicate の戻り値をtrueに固定できます。
実例
一番簡単で一番ありがちなアドバイスの例は、関数の入り口と出口を表示するものでしょう。
/* set global scope */ if (typeof this.window == 'undefined') this.window = this; /* advice */ function printEnterLeave() { var original = arguments.callee.original; var originalName = arguments.callee.originalName; print("Enter into >>> '" + originalName + "'"); var retVal = original.apply(this, arguments); print("Leave from <<< '" + originalName + "'"); return retVal; } /* samples */ function foo() { bar1(); bar2(); } function bar1() { } function bar2() { baz(); } function baz() { zot(); } function zot() { }
上のファイルをロードした状況で、次のようになります。
js> zot() js> foo() js> for (var i = 0, name; name = ['foo', 'bar1', 'bar2', 'baz', 'zot'][i++];) { addAdvice(name, printEnterLeave); } js> zot() Enter into >>> 'zot' Leave from <<< 'zot' js> foo() Enter into >>> 'foo' Enter into >>> 'bar1' Leave from <<< 'bar1' Enter into >>> 'bar2' Enter into >>> 'baz' Enter into >>> 'zot' Leave from <<< 'zot' Leave from <<< 'baz' Leave from <<< 'bar2' Leave from <<< 'foo' js> removeAdvice('zot') js> zot() js>
まとめ
昨日の晩思いついたものだから、実際のところどうだかわからんのだけど、関数呼び出しごとに型チェック、値チェックの追加とか、統計的情報の収集とかに使えそうな気がしないでもない……