日曜プログラミング

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

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 かましてるのは 評価が複数回されるのを避けるためなのかな?

今日はここまで。