読者です 読者をやめる 読者になる 読者になる

暗黙の引数 &env を使ってスコープを曲げてみる

clojure マクロ

昨日に引き続き、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 から参照可能になる。

(with-env   ...)

は、let-envで捕捉したという名前の環境で、式 ...を評価する。ただし、環境に存在しない束縛は、元の環境で評価される。

原理としては、Let Over Lambda の pandoric macroに似た手法を使っていて、let-envの呼び出し箇所で見えている変数を網羅するために&envで渡ってくる環境を使っている。Clojureには代入がないので、値をコピーするだけでいい。

ソースコードは以下のとおり。

local_env.clj

(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))))