Programmer's Note

コード読み書きの備忘録。

Clojureメモ: マクロの動き

マクロの使い方覚え書き。

まず、「プログラミングClojure」に出ていたノーマルな定義。

unlessというマクロ。

(defmacro unless [expr form]
    (list 'if expr nil form))

実行結果

t-macro.core=> (unless false (println "1"))
1
nil
t-macro.core=> (unless true (println "1"))
nil

Clojureのマクロは、Cのプリプロセッサと同様に、 定義された内容はいったん展開され、その後式として評価される。

(macroexpand-1)を使うと展開した後の式が見える。

t-macro.core=> (macroexpand-1 '(unless true (println "1")))
(if true nil (println "1"))

マクロの中身は(list 'if expr nil form)であるが、 このうちexprformは引数として渡され、評価されずにそのまま展開される。

一方、(list ...)は評価されて、これがマクロの結果として返される。 'ifはリーダマクロ'を使ってifを評価せずにそのまま渡している。 nilは評価されるがnilになる。

ここがポイントのようで、Cのプリプロの単純な文字列置換とはちょっと違う。

マクロを以下のように書き換えてみる。unless-2とする。

(defmacro unless-2 [expr form]
    (if expr nil form))

さっきとの違いは、(list...)を外し、'ifではなくifを使った。

実行してみる。

t-macro.core=> (unless-2 false (println "1"))
1
nil
t-macro.core=> (unless-2 true (println "1"))
nil

一見、結果は一緒。

しかし、(macroexpand-1)すると違いが分かる。

t-macro.core=> (macroexpand '(unless-2 false (print "1")))
(print "1")

マクロとして展開された結果が違う。 今度のはifがマクロ展開時に実行されたので、その結果がマクロ展開の結果として返っている。

上記を見ると、マクロ定義は簡単に言えば 何かしらのClojureプログラムのリストを返す関数を作ること、と思えばよさそうだ。

とすると、さきほど一見、unlessunless-2の実行結果が同じに見えたが、 以下を試してみると、、、

t-macro.core=> (unless (= 1 2) (println "1"))
1
nil

t-macro.core=> (unless-2 (= 1 2) (println "1"))
nil

結果は違ってくる。 macroexpand-1してみる。

t-macro.core=> (macroexpand-1 '(unless (= 1 2) (println "1")))
(if (= 1 2) nil (println "1"))

t-macro.core=> (macroexpand-1 '(unless-2 (= 1 2) (println "1")))
nil

unless-2はなぜnilを返すのだろう?

unless-2はマクロとしては、(if '(= 1 2) nil '(println "1"))を実行する形になる。

t-macro.core=> (if '(= 1 1) "yes" "no")
"yes"

t-macro.core=> (if '(= 1 2) "yes" "no")
"yes"

(if..)関数は、評価なしのformをそのまま渡すと true として見なすようだ。

なお、

t-macro.core=> 'true
true
t-macro.core=> 'fase
fase

で、true, falseをquoteしてもtrue, falseになるようだ。 これ忘れるとハマるな。