マクロの暗黙の引数 &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の定義を確認すると、大雑把には

  1. &form と &env という2つの暗黙的な引数を、引数の先頭に追加し、defnを使って通常の関数として定義をする
  2. 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についてはよく分からない。と思って調べていたら、まさに同じ質問がちょうと昨日されていたのを発見した。

Google Groups

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ではなく、今後変更される可能性もある

*1:これは、Varのメタデータに[:macro true]を追加する操作に相当する