マクロの暗黙の引数 &form と &env
Varの適用
Clojureでは、たとえば
(#'list 1 2 3)
が
(list 1 2 3)
と同じ結果を返す。Varに関数が束縛されている場合には、Varに束縛されている関数がそのまま適用されるようだ。
では、Varにマクロが束縛されている場合にはどうなるだろう?マクロを展開した結果が返ってくるだろうか。whenマクロで試してみると以下のようになった。
user=> (#'when '(= 1 1) '(println 'a) '(println 'b)) (if (println (quote b)) (do)) user=> (#'when '(= 1 1) '(println 'a) '(println 'b) '(println 'c)) (if (println (quote b)) (do (println (quote c)))) user=>
結果を見ると、一見うまくいっているように見えるが、どうも最初の2つの引数が無視されているようだ。
defmacroの定義と暗黙の引数 &form と &env
ここで、マクロがどのように定義されるのかを見てみる。defmacroの定義を確認すると、大雑把には
- &form と &env という2つの暗黙的な引数を、引数の先頭に追加し、defnを使って通常の関数として定義をする
- 1. で定義した関数のVarに対してsetMacroを実行する*1
ということをやっている。上のwhenの例で無視されていた引数は、1.で追加される&formと&envではないかと推測できる。試しに以下のようなことをしてみた。
user=> (defn hoge [& args] `'~(vec args)) #'user/hoge user=> (.setMacro #'hoge) nil user=> (hoge 1 (+ 2 3)) [(hoge 1 (+ 2 3)) nil 1 (+ 2 3)] user=>
すべての引数からなるベクタにquoteを付けて返す関数hogeを、setMacroでマクロにする。hoge を (hoge 1 (+ 2 3)) のように呼び出すと、[(hoge 1 (+ 2 3)) nil 1 (+ 2 3)] が結果として返ってくる。返ってきた値の1つめの要素 (hoge 1 (+ 2 3)) が &form で、2つめの要素 nil が &env、3つめ以降の要素がマクロに渡された引数ということのようだ。
&envの正体は?
&formには、名前が表すように、マクロ呼び出しのフォーム全体が渡ってくるようだが、&envの方はいまいちよく分からない。hogeの呼び出しかたを変えて試してみると、
user=> (let [x 0] (hoge 1 (+ 2 3))) java.lang.RuntimeException: Can't embed object in code, maybe print-dup not defined: clojure.lang.Compiler$LocalBinding@4548e798 (NO_SOURCE_FILE:29) user=>
上のようなエラーになる。hogeの展開結果に、コードに埋め込めないオブジェクトが含まれている、ということらしい。
そこで、引数&envに渡ってくる値を単に表示するだけのマクロを定義してみる。
user=> (defmacro fuga [] (println &env) nil) #'user/fuga user=> (let [x 0] (fuga)) {x #} nil user=> (let [x 0] (let [y 1] (fuga))) {y # , x # } nil user=>
出力結果から、&envにはマクロの呼び出しのフォームを囲む静的環境がMapとして渡ってくるようだ。
Mapのキーが、ローカルな束縛の名前を示すシンボルであることは分かるが、値であるLocalBindingについてはよく分からない。と思って調べていたら、まさに同じ質問がちょうと昨日されていたのを発見した。
It contains a LocalBindings object which the compiler uses internally to keep track of a local binding. Note that unlike the keys, the values of &env are not a stable API, they're implementation details and may well change. When he added &env I think Rich said he'd look at giving the values of &env a proper API as part of the future Clojure in Clojure compiler.
LocalBinding自体は今はまだ正式なAPIではないようだが、LocalBindingからは束縛が関数の引数か否かといった情報や、束縛の初期化式等が得られるようになっている。これがマクロを通してユーザから利用できるようになるとすれば、かなり強力な仕組みになるだろう。
まとめ
というわけで、今回はこんなことが分かった。
- defmacroでマクロを定義すると、暗黙の引数 &form と &env が追加される
- &formにはマクロ呼び出しのフォーム全体が、&envにはマクロ呼び出しを囲む静的環境が、それぞれ渡ってくる
- &envで渡ってくるMapの値であるLocalBindingからは、束縛に関する種々の情報を得ることができる
- ただし、まだ正式なAPIではなく、今後変更される可能性もある