valid,invalid

関心を持てる事柄について

Clojure で初めての unless マクロを書く

7つの言語 7つの世界』を読みつつ Clojure 入門している。その中でマクロに関する記述があったが LISP のマクロに馴染みがないので、コードを見ても何が起きているのか瞬時にわからなかった。

正解のコードを少しずついじりながら、なぜそう書かないといけないのか、なぜそう書くと動くのかを確かめてみた。

ちなみに例は unlessで、これはマクロ入門でおなじみらしい。

失敗例

まずは挙げられているダメな例から。

(defn unless [test body] (if (not test) body))
#'user/unless

(unless true (println "Lose Yourself"))
Lose Yourself
nil

body が評価されてしまっている。testfalse のとき以外は評価してほしくない。なのでこれがダメなのはわかりやすい。

正規順序でなく作用的順序で評価してしまうと無限ループが発生してしまう例が SICP にもあった(気がする)。

正しい unless

以下のようになるようだ。

(defmacro unless [test body]
  (list 'if (list 'not test) body))

これをいじってみる。

if を即時評価していないのはなぜ

'if の部分は以下のように書けるのではないだろうか?

(defmacro unless [test body]
  (if (list 'not test) body))

(unless true (println "Nothing to tell."))
Nothing to tell. ;; 評価されてしまっている
nil

(unless false (println "This is it."))
This is it.
nil

はい、body が評価されてしまっているダメ。

(macroexpand '(unless cond body))
body

上記の unless をマクロ展開してみると body を実行するだけのマクロになってしまっていることがわかる。

not を即時評価していないのはなぜ

では、'not の部分は以下のように書けるのではないだろうか?

(defmacro unless [test body]
  (list 'if (not test) body))

(unless true (println "Nothing to tell."))
nil

(unless false (println "This is it."))
This is it.
nil

一見成功しているように見える…!

だがそれが命取り…!実は駄目…!

上記の unless をマクロ展開してみると条件部が既に評価されてしまっていることがわかる。

(macroexpand '(unless condition body))
(if false body)

;; 条件は false になるのに println が実行されない
(unless (= 1 2) (println "This is it."))
nil

正しい unless をマクロ展開すると条件部がまだ評価されていないことがわかる。

(macroexpand '(unless cond body))
(if (not cond) body)

;; ちゃんと unless 呼び出し時に評価されている
(unless (= 1 2) (println "This is it."))
This is it.
nil

defn で書いたらどうなる?

(defn unless [test body]
  (list 'if (list 'not test) body))

(unless true (println "Nothing to tell."))
Nothing to tell.
(if (not true) nil)

(unless false (println "This is it."))
This is it.
(if (not false) nil)

(class (unless false (println "This is it.")))
This is it.
clojure.lang.PersistentList

(list 'if (list 'not test) body) が評価されてしまった後のリストが返ってくる。body も評価されてしまっているので println が実行されている。

関連する関数などの知識

quote

評価してほしくないフォームに quote 関数を適用すると評価を抑制できる。

ダメなマクロの例を成り立たせるために body' で遅延評価させられる。もちろん呼び出し側でコントロールしているのでよくない。

(defn unless [test body] (if (not test) body))
#'user/unless

(unless true (quote (println "Lose Yourself")))
nil

;; ' 記号でも書ける
(unless true '(println "Lose Yourself"))
nil

defmacro

defmacrodefn のように関数を定義するが、その戻り値は実行時にコンパイラにマクロとして解釈されるようになる。

(doc defmacro)
-------------------------
clojure.core/defmacro
([name doc-string? attr-map? [params*] body] [name doc-string? attr-map? ([params*] body) + attr-map?])
Macro
  Like defn, but the resulting function name is declared as a
  macro and will be used as a macro by the compiler when it is
  called.
nil

;; 確かに Macro と解釈されているようす
(doc unless)
-------------------------
user/unless
([test body])
Macro
  nil
nil

7つの言語 7つの世界

7つの言語 7つの世界