日曜プログラミング

休日趣味でやってるプログラミング関連記事をダラダラと

On Lisp -> Clojure へ移植: 6.1~6.3

6 マクロ

6.1 マクロはどのように動作するか

p54

(defmacro nil! [x]
  (list 'def x nil))

6.2 逆クォート

そのままバッククォートと言われたりもする。

一応補足すると、Common Lisp でのバッククォート中のカンマは Clojure では ~ に相当する。 同様に Common Lisp,@Clojure では ~@ となる。

対比して書くとこんな感じ。

項目 Common Lisp Clojure
クォート ' '
バッククォート ` `
アンクォート , ~
スプライシングアンクォート ,@ ~

アンクォートの文字が違うだけですな。

p56

バッククォート版 nil!

(defmacro nil! [x]
  `(def ~x nil))

p57

図 29 マクロ定義に逆クォートを使った例と使わない例.
(defmacro nif [expr pos zero neg]
  `(cond
    (pos? ~expr) ~pos
    (zero? ~expr) ~zero
    (neg? ~expr) ~neg))

(defmacro nif [expr pos zero neg]
  (list
   'cond
   (list 'pos? expr) pos
   (list 'zero? expr) zero
   (list 'neg? expr) neg))

Common Lisp の truncate や signum と全く同じものはないので別実装。

以下テストコード。

(map #(nif % 'p 'z 'n) [0 2.5 -8])
;=> (z p n)

when。どうでもいいんだけど Clojure の when はなぜか逆クォートを使わない版で書かれてる。

(defmacro our-when [test & body]
  `(if ~test (do ~@body)))

p58

(defn greet [name]
  `(hello ~name))
少し脱線

一つ Clojure で注意する点があるとすると バッククォート中でクォートされてないシンボルがあった場合まず名前解決しようとする。 何気なく定義した上の関数の結果を見るとそれが分かる。

(greet 'Clojure)
;=> (user/hello Clojure)

動きとしてはまず既に定義済みの名前がないか探し、なければ現在の名前空間で修飾して 展開するようだ。この展開された式を評価してみても user 名前空間に hello なんてない よと怒られる。

既に定義済みの関数を使ったものをバッククォート中に含めてみる。

(defn mac-factorial [n]
  `(* ~@(range 1 (inc n))))

(mac-factorial 10)
;=> (clojure.core/* 1 2 3 4 5 6 7 8 9 10)

も一つ Clojure 特有と言うわけじゃないんだろうが defn と defmacro の違いは defn は定義した式を評価すると言うのに対して defmacro は展開できるところまで 式を展開した後に評価すると言う点が違うと言う事でいいのかな。

先と中身が全く同じものを defmacro で定義して評価してみる。

(defmacro mac-factorial [n]
  `(* ~@(range 1 (inc n))))

(mac-factorial 10)
;=> 3628800

6.3 単純なマクロの定義

member の定義だが、同等の事は Clojure なら drop-while で可能。

(defn member [item col & {:keys [test] :or {test =}}]
  (seq (drop-while #(not (test item %)) col)))

Clojure では nil と () が違うものなので一応 seq をかましておく。 Common Lisp 標準だとキーワード引数として他にも取れるものがあるのだが ここでは省略する。

また、Common Lisp の同一性チェックは案外色々あって面倒なので、 eql は Clojure の =, eq は Clojure の identical? と置き換えたものとして memq 定義をする。

p59

(defmacro memq [x choices]
  `(member ~x ~choices :test identical?))

それと本書の方では #' が出てるが Clojure のそれとは異なり、 Common Lisp では関数呼び出しをするリードマクロな点に注意するくらいか。 これは Common Lisp では言語仕様として変数と関数の名前空間が異なる事に起因する。 他の Lisp との違いについては Clojure 公式の下記 URL に説明がある。

次に while マクロの例があるが、これまた Common Lisp の do はマクロでありそのままのものはない。 が、while は Clojure なら条件付きの loop として展開させてやれば良い。

(defmacro while [test & body]
  `(loop []
     (if ~test
       (do
         ~@body
         (recur)))))

後で Clojure の方のソース見て when の存在を忘れてた事に気づいたw

今日はここまで。