『On Lisp』を読んでいて出てきた「アナフォリックマクロ」(ある種の"不健全な"マクロ)をClojureでも書いてみた。
ついでに、Common LispとClojureでのマクロの「変数捕捉」についても確かめてみた。
アナフォリックマクロ
アナフォリックマクロ(anaphoric macro; 前方照応的なマクロ)とは、通常は有害なマクロの「変数捕捉」(variable capture)を意図的に発生させることによって前方照応的なシンボル(≒ 自然言語における代名詞)を扱えるようにしたマクロのこと。
ちなみに、変数捕捉を起こしえない安全なマクロは「健全なマクロ(hygienic macro)」と呼ばれる。
マクロの変数捕捉
壊れた if-not
/ unless
マクロを通してマクロの変数捕捉が発生する仕組みを確認してみる。
Common Lispの場合
Common Lispで以下のようなマクロを定義したとする。
;; Common Lisp
(defmacro if-not (test then else)
`(let ((x ,test)) ; 通常ここでletは必要ないがシンプルなマクロの例として
(if (not x)
,then
,else)))
定義したマクロを次のように利用してみると、
;; Common Lisp
> (let ((x 10))
(if-not nil
(format t "x: ~a~%" x)
(princ "TRUE")))
x: NIL ; 標準出力
NIL ; 評価結果
マクロ利用者の期待に反して、変数 x
の値が nil
と出力されてしまう。
原因を確認するために、この式を再帰的にマクロ展開してみると、
;; Common Lisp
> (macroexpand-all '(let ((x 10))
(if-not nil
(format t "x: ~a~%" x)
(princ "TRUE"))))
(LET ((X 10))
(LET ((X NIL))
(IF (NOT X)
(FORMAT T "x: ~a~%" X)
(PRINC "TRUE"))))
マクロ利用時に使っている変数 x
とif-notマクロの定義で利用した変数 x
が衝突してしまうことがこの挙動の原因であることが分かる。
このような現象をマクロの変数捕捉(variable capture)という。
※ Common Lispでの変数捕捉のその他の例と回避方法については『On Lisp』第9章などを参照。
Clojureの場合
Clojureでも同様のマクロを定義してみる。
;; Clojure
(defmacro unless [test then else]
`(let [x# ~test] ; 通常ここでletは必要ないがシンプルなマクロの例として
(if (not x#)
~then
~else)))
これを先ほどのCommon Lispの例と同じように利用してみると、
;; Clojure
> (let [x 10]
(unless false
(printf "x: %s%n" x)
(println "TRUE")))
x: 10 ; 標準出力
nil ; 評価結果
マクロ利用者の期待通りの結果が出力される。
これは、Clojureでは以下のようにマクロ展開されるため。
;; Clojure
> (macroexpand-all '(let [x 10]
(unless false
(printf "x: %s%n" x)
(println "TRUE"))))
(let* [x 10]
(let* [x__1291__auto__ false]
(if (clojure.core/not x__1291__auto__)
(printf "x: %s%n" x)
(println "TRUE"))))
変数 x
の衝突は未然に回避されている。
Clojureのマクロでは、シンタックスクォートすると、
- シンボルがそのコンテキストに基づく名前空間で修飾される
- e.g. 上記の
clojure.core/not
- e.g. 上記の
-
シンボル#
という形式のローカルな束縛がauto-gensymされる- e.g. 上記の
x__1291__auto__
- e.g. 上記の
この仕組みにより、Clojureではマクロの変数捕捉が発生しにくくなっている。
Clojureで変数捕捉
先ほどの unless
マクロを少し書き換えた unless'
を新たに定義してみる。
;; Clojure
(defmacro unless' [test then else]
`(let [~'x ~test] ; 通常ここでletは必要ないがシンプルなマクロの例として
(if (not ~'x)
~then
~else)))
このように、ローカルな束縛を ~'シンボル
という形式(つまり、クォートしてアンクォート)で定義すると(ここでは ~'x
)、
;; Clojure
> (macroexpand-all '(let [x 10]
(unless' false
(printf "x: %s%n" x)
(println "TRUE"))))
(let* [x 10]
(let* [x false]
(if (clojure.core/not x)
(printf "x: %s%n" x)
(println "TRUE"))))
変数 x
が名前空間で修飾されることもauto-gensymされることもなくそのままの形でマクロ展開結果に現れる。
この式を実際に評価してみると、
;; Clojure
> (let [x 10]
(unless' false
(printf "x: %s%n" x)
(println "TRUE")))
x: false ; 標準出力
nil ; 評価結果
上述のCommon Lispコードと同様に変数捕捉が発生していることが分かる。
アナフォリックな if
(= aif
)
通常はバグの原因になりうるマクロの変数捕捉を意図的に発生させ、有効利用するアナフォリックマクロ(anaphoric macro)を書いてみる。
『On Lisp』第14章で紹介されているアナフォリックマクロのひとつ aif
は
(let ((x (f)))
(if x
(g x)
(h)))
のようなパターンを抽象化し、
(aif (f)
(g it)
(h))
と書けるようにするもの。
つまり、テスト対象の式(ここでは (f)
)をマクロで暗黙に定義される it
で受けて再利用できるようにする。
この aif
はCommon Lispで例えば以下のように実装できる。
;; Common Lisp
(defmacro aif (test then &optional else)
`(let ((it ,test))
(if it
,then
,else)))
シンタックスクォート内でローカル束縛しているシンボル it
が変数捕捉されることを利用して、代名詞のように使うことを可能にしている。
aif
をClojureで実装してみると、例えば次のようになる。
;; Clojure
(defmacro aif
([test then]
`(aif ~test ~then nil))
([test then else]
`(let [~'it ~test]
(if ~'it
~then
~else))))
シンタックスクォート内で ~'it
とすることで意図的に変数捕捉を発生させている。
これを利用すれば、
(let [x (f)]
(if x
(g x)
(h)))
のようなパターンを
(aif (f)
(g it)
(h))
と書ける。
実際には、 clojure.core/if-let
マクロを利用すれば
(if-let [it (f)]
(g it)
(h))
と、ユーザ側で明示的に任意の名前(ここではたまたま it
)で束縛を作って使うことができ、 Clojureでは一般にこちらのスタイルが好まれる。
その他の典型例
アナフォリックな when
(= awhen
)
;; Common Lisp
(defmacro awhen (test &body body)
`(aif ,test
(progn ,@body)))
;; Clojure
(defmacro awhen [test & body]
`(aif ~test
(do ~@body)))
;; 利用例
(awhen (:fr {:en "Hello" :fr "Bonjour" :ru "Здравствуйте"})
(str it ", Clojure!"))
;; => "Bonjour, Clojure!"
cf. 明示的な名前束縛で同等のことができるマクロ: clojure.core/when-let
アナフォリックな lambda
(= alambda
)
;; Common Lisp
(defmacro alambda (params &body body)
`(labels ((self ,params
,@body))
#'self))
;; Clojure
(defmacro alambda [params & body]
`(letfn [(~'self ~params
~@body)]
~'self))
;; 利用例
(def fib (alambda [n]
(if (< n 2)
1
(+ (self (- n 2))
(self (- n 1))))))
(map fib (range 10))
;; => (1 1 2 3 5 8 13 21 34 55)
cf. 明示的な名前束縛で同等のことができるマクロ: clojure.core/fn
応用例
Clojure標準ライブラリのthreading macro clojure.core/->
は以下のように定義されている。
;; Clojure
(defmacro ->
"Threads the expr through the forms. Inserts x as the
second item in the first form, making a list of it if it is not a
list already. If there are more forms, inserts the first form as the
second item in second form, etc."
{:added "1.0"}
[x & forms]
(loop [x x, forms forms]
(if forms
(let [form (first forms)
threaded (if (seq? form)
(with-meta `(~(first form) ~x ~@(next form)) (meta form))
(list form x))]
(recur threaded (next forms)))
x)))
このマクロ定義をもとに、アナフォリックな ->
を書いてみた(仮に =>
と命名する)。
;; Clojure
(defmacro => [x & forms]
(loop [x x
forms forms]
(if forms
(let [form (first forms)
threaded (if (seq? form)
(with-meta (if (->> form
flatten
(some #{'it}))
`(let [~'it ~x]
~form)
`(~(first form) ~x ~@(next form)))
(meta form))
(list form x))]
(recur threaded (next forms)))
x)))
;; 利用例
(=> (range 2)
(concat [2 3])
(map inc it)
(concat it [5] (reverse it)))
;; => (1 2 3 4 5 4 3 2 1)
cf. 明示的な名前束縛で同等のことができるマクロ: clojure.core/as->
基本的には clojure.core/->
と同じように振る舞うが、シンボル it
が含まれるリストを見つけたら、 it
を直前の式の値に置き換えるというもの。
まとめ
- Clojureでも
~'シンボル
という形式を利用することでアナフォリックマクロを定義できる - 一般には
if-let
,when-let
のようにユーザ側で明示的な束縛を作るマクロのほうが好まれるが、ライブラリ/DSL構築のための実装技術として役に立つ(かも)
Further Reading
書籍
- 『On Lisp』
-
The Joy of Clojure, Second Edition
- 8.5 Using macros to control symbolic resolution time