暗黙の引数 &env を使ってスコープを曲げてみる
昨日に引き続き、Clojureマクロの暗黙の引数&envについて。
&envを通してマクロ呼び出しのフォームを囲む環境が手に入るわけで、これを利用しておもしろいことはできないだろうか。そう考えているうちに、スコープを曲げる - 主題のない日記で出てきた例を思い出した。こんな例だ。
(let ((x 1)) (let/scope d1 (let ((x 2)) (let/scope d2 (let ((x 3)) (list (d1 x) (d2 x) x)))))) ;=> (1 2 3)
この例では、Schemeのマクロを使って、本来シャドウイングされて見えないはずの外の環境にアクセスできるようにするlet/scopeを定義している。
これと似たようなことをClojureで実現してみる。
(let [x 1] (let-env d1 (let [x 2] (let-env d2 (let [x 3] [(with-env d1 x) (with-env d2 x) x])))))
ここでの肝は、let-env と with-env という2つのマクロだ。
(let-env...)
は、現在見えている環境を捕捉し、
(with-env...)
は、let-envで捕捉した
原理としては、Let Over Lambda の pandoric macroに似た手法を使っていて、let-envの呼び出し箇所で見えている変数を網羅するために&envで渡ってくる環境を使っている。Clojureには代入がないので、値をコピーするだけでいい。
ソースコードは以下のとおり。
(ns local-env (:use [clojure.contrib.def :only (defvar-)]) (:use [clojure.contrib.macro-utils :only (mexpand-all symbol-macrolet)])) (defvar- *local-envs* {}) (defmacro let-env [env-name & body] (binding [*local-envs* (assoc *local-envs* env-name &env)] `(let [~env-name ~(into {} (for [x (keys &env)] `['~x ~x]))] ~(mexpand-all `(do ~@body))))) (defmacro with-env [env-name & body] (let [env (*local-envs* env-name)] (mexpand-all `(symbol-macrolet ~(vec (mapcat (fn [name] `[~name (~env-name '~name)]) (keys env))) ~@body))))