Clojureでアスペクト指向プログラミング(その2)
前回の続きです。
とはいっても、実は私、Clojureについては「プログラミングClojure」で学んだだけ(しかも、Common LispもSchemeも経験がない)という、とても弱っちい状態なんですよ。
ですから、Clojureでアスペクト指向する際には、それはもーいろいろと試行錯誤しました。で、今回の投稿では、私がClojureでアスペクト指向をするために辿った経緯をそのまま書いてみます。
読むのに時間がかかる上に、分かっている人には当たり前の話が多いとは思いますけど、Clojureの学習のやり方として参考になるかもしれませんから、どうかご容赦の程をお願いいたします。
基本方針
アスペクト指向プログラミングを実現するには、関数の実体を取得して、その実体の始めか終わりにアスペクトとして実行したい処理を追加するだけでよいはず。
ただ、残念なことにそのやり方が皆目検討つきません……。そこで、Webブラウザを起動してhttp://www.clojure.org/を開いて、[Reference]リンクをクリックしました。原典をあたるのは基本ですもんね。
英語に四苦八苦しながら上から読んでいくと「Vars and the Global Environment」に書いてある内容が、アスペクト指向プログラミングに役立ちそうです。書いてある内容は以下の通り。
Functions defined with defn are stored in Vars, allowing for the re-definition of functions in a running program. This also enables many of the possibilities of aspect- or context-oriented programming. For instance, you could wrap a function with logging behavior only in certain call contexts or threads.
defnで定義された関数はVarに格納され、プログラム実行中に再定義することが可能である。このことは、アスペクト指向プログラミングやコンテキスト指向プログラミングを可能にする。たとえば、ある呼び出しコンテキストやスレッドの中でのみ、ログ出力でその関数を囲むことを可能にできるのだ。
翻訳には自信がないけれど、aspectという言葉が入っているから、多分そのものズバリのはず。というわけで、Varについて調べれば「関数の実体を取得する」のは実現できそうです。
さらにその文書の少し前の部分には、with-redefsやwith-redefs-fnは静的なVarsを再定義する目的で提供されていると書いてあります。with-redefsやwith-redefs-fnがどうやって再定義しているかを調べれば、「関数の前後に処理を追加する」のも出来そうです。
はい、これで基本方針が決まりました。
関数の実体を取得する
でも、実はVarが何なのか分かりません……。たぶん、VARiableのvarなんだろうなぁ、という程度の知識しかないんですよ、私ってば。
でも平気。分からないことは調べればよいんですから。私はプログラマなので、プログラム・コードのレベルで調べた方が効率がよい。ただし、何も指針が無い状態で調査するのはあまりにも大変ですから、まずはCheat Sheetを開きました。Clojureの場合はhttp://clojure.org/cheatsheetですな。
そのCheat Sheetの中の「Vars and global environment」を見ると、var?関数を使えばVarなのかそうでないのかを判断できそうです。REPLを開いて、さっそく実験してみます。
user> (def x 1) #'user/x user> (var? x) false
ええっ?xはVarじゃないの?と、混乱しないで、慌てず騒がずClojureDocsのサイトでvar?について調べます(Cheat Sheetのvar?をクリックすると、ClojureDocsのvar?について述べているページに遷移します)。
ClojureDocsに挙がっている例(例を書いてくださった方、ありがとうございます)を見ると、(var 名前)とやらないとVarとして扱われないことが分かりました。で、このvarスペシャル・フォームのリーダー・マクロが「#'」。というわけで、再実験します。
user> (var? (var x)) true user> (var? #'x) true
はい。trueが返って来ました。ついでなので、このxってのは何なのかも調べてみましょう。ClojureはJavaの上に作られていますから、classを問い合わせればわかるはず。
user> (class (var x)) clojure.lang.Var user> (class x) java.lang.Long
はい、xに束縛された値である1のclassはLongなのですね。……私はこんなことが知りたかったわけじゃないやい!というわけで、少しふてくされて、clojure.orgのReferenceに戻ります。
そうしたら、「Vars and the Global Environment」の中に面白い記述を見つけました。
The Namespace system maintains global maps of symbols to Var objects (see Namespaces).
名前空間システムは、シンボルからVarへのグローバルなマップを管理する(名前空間を参照)。
ということは、xをシンボルに変換して、それをキーにすれば名前空間が管理するマップからVarを取得できるってことになります。xがいったい何なのかは気にしないことにして、この有望な情報をREPLで試してみましょう。
Cheat Sheetによれば、ns-interns関数で名前空間にインターン(名前空間が管理するマップにシンボルとVarを対応付けて登録する指す言葉らしい)されたシンボルとVarを取得できるみたい。REPLを再立ち上げして、さっそく試してみます。
user> (def foo "f") #'user/foo user> (defn bar [] "b") #'user/bar user> (ns-interns 'user) {bar #'user/bar, foo #'user/foo}
おお、シンボルとVarのマップを取得できました。更に試してみます。
user> (def b (get (ns-interns 'user) 'bar)) #'user/b user> (b) "b"
素晴らしい。取得したVarを関数として呼び出せました。そうそう、アスペクト指向プログラミングは変数の場合は何もする必要がないわけですから、関数かどうかの判定が必要ですよね。試してみます。
user> (fn? b) false
……あれ、関数として呼び出せたのに、関数ではないの?混乱してきました。可能性は2つ。1.fn?は関数かどうかを返す関数ではない、2.実はbは関数ではない。馬鹿らしいとは思いつつも、1を試してみます。
user> (fn? bar) true
関数を引数に指定したら、fn?はtrueを返してきました。ということは「実はbは関数ではない」のでしょう。別の角度からもう一度確認してみます。
user> bar #<user$bar user$bar@7e896e10> user> b #'user/bar
あらら、確かに違いそうですね。よく考えてみたら、(var bar)した結果がVarなわけで、bはVarなのですから、varの反対向きの操作が必要なはずです。でもその方法が分かりません……。JavaとかC#なら、メソッド一覧をIDEが出力してくれるのでしょうけど、Clojureではそんなのは無理ですし……。
というわけで、我々が大好きなソース・コードを見てみます。Clojureのすべてのコードは、https://github.com/clojure/clojure.gitからダウンロードできます。ダウンロードして、clojure/src/jvm/clojure/lang/Var.javaを開いてみました。
すると、ありました。「final public Object deref()」というメソッドが定義されています。derefってのはRefやAgent、Atomから値を取り出すときに使う関数ですから、目的に合致しそう。しかも、さっき読んだ「Vars and the Global Environment」の一行目に、VarはRefやAgent、Atomの仲間だとも書いてありました。
ただ、念の為に、clojure/src/clj/clojure/core.cljを開いて、deref関数の定義部分を見ておきましょう。
(defn deref ([^clojure.lang.IDeref ref] (.deref ref)) ([^clojure.lang.IBlockingDeref ref timeout-ms timeout-val] (.deref ref timeout-ms timeout-val)))
おお、Javaで定義されたderefメソッドを読んでいるだけだ。derefで正しいという根拠が増えたことになるので、もう我慢できずに試してみます。
user> (fn? (deref b)) true
やったよママン!fn?でtrueが返ってきました。これで関数の実体を手に入れたことになります。
関数の前後に処理を入れる
関数の実体は手に入ったけれど、でも、どうやって関数の前後に処理を入れればいいのでしょうか?いったい関数の中身はどういう形になっているのかなぁ。空の向こうには何があるのかなぁ。clojure.orgのReferenceを見ても、関数の中身がどうなっているのかは書いていないなぁ……。
と、どうにもならなくなってしまった場合は、基本の基本に立ち返りましょう。Google先生です。ここまでGoogle先生に頼らないようにしていたのは、Google先生は優秀すぎるので、Clojureでのアスペクト指向プログラミングの実現方法のすべてを教えてくれちゃう危険性があったから。いろいろ考えるのが楽しいんですから、答えを見つけちゃう可能性がある行動は避けなければなりません。
でも、背に腹は変えられないですので、Google先生で検索してみます。「clojure aspect oriented programming」で検索します。
はい、一番上のhttp://stackoverflow.com/questions/5573418/aspect-oriented-programming-in-clojureに、関数に処理を追加する方法が載っていました。ラムダ関数を作ってしまえば良いんですね。さっそく試してみます。
user> (def b' (fn [& args] (do (println "BEFORE") (apply b args)))) #'user/b' user> (b') BEFORE "b"
BEFOREと出力されています。やりました!関数の前に処理を入れ込むことができました。あとは、作成したラムダ関数で最束縛する方法を調べるだけです。
で、最初の方で述べたように、関数を再束縛する方法はwith-redefsやwith-redefs-fnを調べれば分かりそうです。ですから、ソース・コードを見てみることにしましょう。clojure/src/clj/clojure/core.cljを開きます。
with-redefsマクロはその中でwith-redefs-fn関数を呼び出しているだけなので、with-refefs-fnの方をじっくりと調べます。with-redefs-fnの中身は、以下の通り。
(defn with-redefs-fn [binding-map func] (let [root-bind (fn [m] (doseq [[a-var a-val] m] (.bindRoot ^clojure.lang.Var a-var a-val))) old-vals (zipmap (keys binding-map) (map deref (keys binding-map)))] (try (root-bind binding-map) (func) (finally (root-bind old-vals)))))
このコードから、以下の2つのことが分かりました。
1.derefで関数を取り出すという、先ほど推測した方式は正しかった(old-valsのコードでderefを使用しているため)。
2.再束縛は、VarクラスのbindRootメソッドを呼べば出来そう(だって、それ以外の処理はしていないですもん)。
念の為、Java側のbindRootメソッドの中身も読んでみます。
synchronized public void bindRoot(Object root){ validate(getValidator(), root); Object oldroot = this.root; this.root = root; ++rev; try { alterMeta(dissoc, RT.list(macroKey)); } catch (Exception e) { throw Util.sneakyThrow(e); } notifyWatches(oldroot,this.root); }
うぅ、Javaのコードは分かりづらい……。正直、全く分かりません。Javaで検証するのはきっぱり諦めて、Clojureのコードを書いて検証することにしました。
ま、多分平気でしょ。関数の前後に処理を入れる方法は分かったことにしてしまいます。間違えていたらやり直せばいいんですしね。
Clojureでアスペクト指向プログラミング
以上、必要な調査が終わりましたので、コードを書いてみます。今回は遊びなので、機能は簡単にしました。名前空間と関数を指定したら、その名前空間の中にある関数を実行する前に、指定した関数が実行されることにします。
というわけで、作成したコードはこんな感じ。
(ns clj-aspect.core) (defn weave-aspect [namespace aspect-fn] (doseq [v (->> (ns-interns namespace) (vals) (filter #(fn? @%)))] (let [f (deref v)] (.bindRoot v (fn [& args] (do (aspect-fn) (apply f args)))))))
今回は、with-redefsにあった元に戻す処理は省略しました。元に戻す処理を書いちゃうとwith-redefsとほとんど同じになっちゃって、もしほとんど同じならばwith-redefsを呼び出す形で実装すればよいことになっちゃって、その場合は一番面白い部分のコードを書けなくなっちゃいますからね。
コードが書けたので、REPLで動かして試してみます。
user> (use 'clj-aspect.core) nil user> (defn foo [] (println "FOO")) #'user/foo user> (def bar "BAR") #'user/bar user> (weave-aspect 'user #(println "ASPECT")) nil user> (foo) ASPECT FOO nil user> bar # 変数は影響を受けていないことを確認します。 "BAR"
やったぁ!動きました。FOOの前にASPECTが出力されています!プログラムとしてテストを書いてきちんとテストして、injectする関数を正規表現で指定できるようにして、前だけじゃなくて後にも差し込みできるようにして、引数をアスペクトにも渡すようにして、で、状態を元に戻す処理を追加してあげれば、それなりに使えるライブラリになるかもしれません。そのうち、暇ができたらやってみることにしましょう。
ともあれ、本を一冊読んだだけの頭が硬くなっているおっさんの私でもアスペクト指向プログラミングのライブラリ(の雛形ですが)を作れちゃうんですから、Clojure関連の文書と、オープン・ソースなのでソース・コード見放題ってことと、あともちろんREPLは素晴らしいですな!