On Lisp -> Clojure へ移植: 6.4~6.8
6.4 マクロ展開の確認
p60
図 33 マクロ展開確認用のマクロ
(defmacro mac [expr] `(clojure.pprint/pprint (macroexpand-1 '~expr)))
cider なら C-c C-m すればいいんだが一応。
6.5 引数リストの構造化代入
p61
最初サンプルほぼそのまま書いてみたのだが、
(defmacro our-dolist [[v coll & result] & body] `(do (map (fn [~v] ~@body) ~coll) (let [~v nil] ~result)))
nil しか返らない。
(our-dolist [x '(a b c)] (print x)) ;=> nil
もしや map って lazy-seq だから何もしてないのかと思い doall をかましてみると abc も出た。
(defmacro our-dolist [[v coll & result] & body] `(do (doall (map (fn [~v] ~@body) ~coll)) (let [~v nil] ~result)))
when-bind サンプル。Clojure の when-let と動作は多分同じ。
(defmacro when-bind [[v expr] & body] `(let [~v ~expr] (when ~v ~@body)))
ちなみに Clojure のソースを見ると入れ子の destructuring は使わずに実装してる。 1.0 の頃だとまだなかったのかな?
(when-let [bindings & body] ;; 途中省略 (let [form (bindings 0) tst (bindings 1)] `(let [temp# ~tst] (when temp# (let [~form temp#] ~@body)))))
6.6 マクロのモデル
p62
図 34 defmacro の概形のコードがあるがここは一旦飛ばす。 何となくわからん気がしないでもないが、destructuring-bind 辺りや gemsym した g を仮引数に関数作ってる辺りが今の所理解できん。
次にあるのはレキシカル環境を読めるはずだというサンプル。
(let [op 'def] (defmacro our-def [sym init] (list op sym init)))
一見使えそうだが、On Lisp で例示している事と全く同じかどうかは自信なし。
(our-def a 1) ;=> #'user/a a ;=> 1
6.7 プログラムとしてのマクロ
p63
まず Common Lisp がやってる do マクロについて軽く読み解く。 これはすごく大雑把に言えば Clojure の loop が少し複雑になったものと言える。
(do ;; 初期化ブロック。 ;; sym [init] [step] と言った最大 3 つをペアとするリストを並べる。 ;; sym がローカル変数、init が初期値、step がループ更新毎に変数をどう変化させるかの式。 ;; init, step は任意で、init のみの場合はループ毎の更新無し、 ;; init も step もなければ変数は nil で初期化される。 ((w 3) (x 1 (1+ x)) (y 2 (1+ y)) (z)) ;; ループ終了条件と条件満たした時の処理。 ;; 最後の式が do マクロが返す値になる。 ((> x 10) (princ z) y) ;; ループ本体。初期化ブロックでのローカル変数更新式で ;; ローカル変数を更新しながら終了条件を満たすまで実行される。 (princ x) (princ y))
やや限定的になるが、初期値と変数変化を必須にすれば割とすんなり loop に展開できる。
(loop [w 3 x 1 y 2 z nil] (if (> x 10) (do (println z) y) (do (println x) (println y) (recur w (inc x) (inc y) z))))
上の式に展開するマクロはこんな感じにしてみる。不格好だが初期化ブロック、 終了条件・終了時処理ブロック、ループ本体ブロックは何となく [] で囲んだ。
(cl-like-do [w 3 w x 1 (inc x) y 2 (inc y) z nil z] [(> x 10) (println z) y] [(println x) (println y)])
一応本書の少し先にある図 36 実装例を見ないで書いてみたもの。
(defmacro cl-like-do [inits end-clause & body] (let [binds (mapcat butlast (partition 3 inits)) steps (take-nth 3 (subvec inits 2))] `(loop [~@binds] (if ~(first end-clause) (do ~@(rest end-clause)) (do ~@body (recur ~@steps))))))
実装例見た後、ああ end-clause は destructuring してしまってもいいなと思い少し書き直したもの。
(defmacro cl-like-do [inits [test & result] & body] (let [binds (mapcat butlast (partition 3 inits)) steps (take-nth 3 (subvec inits 2))] `(loop [~@binds] (if ~test (do ~@result) (do ~@body (recur ~@steps))))))
本当は make-initforms と make-stepforms をちゃんと作った方がいいんだろうけど do 自体にあまり利便性を見出せないのでこのくらいで次に行く。
6.8 マクロのスタイル
p64
On Lisp 実装例 2 種(図 37 and と等価なマクロの例 2 個.)。 Common Lisp と違い t と言う唯一のシンボルはないので、全て true なら 最後の式の値を返すようにした。
(defmacro our-and [& args] (case (count args) 0 args 1 (first args) `(when ~(first args) (our-and ~@(rest args))))) (defmacro our-andb [& args] (if (nil? (seq args)) args (letfn [(expander [rest] (if (next rest) `(if ~(first rest) ~(expander (next rest))) (first rest)))] (expander args))))
our-andb ってちゃんと展開されるのと見たが展開されとる。
(macroexpand-1 '(our-andb 1 2 3 nil 5)) ;=> (if 1 (if 2 (if 3 (if nil 5)))) (our-andb 1 2 3 nil 5) ;=> nil
うーん、後のは今後思いつくパターン出るんだろうか。ただ、前者はマクロには recur 使えないから Clojure で再帰的なマクロ書きたい場合はこうなるのかなあ。
最後に Clojure の and を見てみる。
(defmacro and ([] true) ([x] x) ([x & next] `(let [and# ~x] (if and# (and ~@next) and#))))
引数での多重定義できる Clojure ならこの形がいいな。
(if ~x (and ~@next) ~x)
とせずに let かましてるのは
評価が複数回されるのを避けるためなのかな?
今日はここまで。