Common Lisp のマクロが何をやってるか把握する

Common Lisp のマクロの背後で何が行われているのかがいまいち分からなかったので、少し詳しく見ていくことにした。

On Lisp7章 マクロ には、defmacromacroexpand-1 の内部処理を Lisp でエミュレートしたコードが紹介されている。

(defmacro our-expander (name) `(get ,name 'expander))

(defmacro our-defmacro (name parms &body body)
  (let ((g (gensym)))
    `(progn
       (setf (our-expander ',name)
             #'(lambda (,g)
                 (block ,name
                        (destructuring-bind ,parms (cdr ,g)
                          ,@body))))
       ',name)))

(defun our-macroexpand-1 (expr)
  (if (and (consp expr) (our-expander (car expr)))
      (funcall (our-expander (car expr)) expr)
      expr))

7章の冒頭に登場する nil!our-defmacro を使って定義したとしよう。

(our-defmacro nil! (var) `(setq ,var nil))

このとき、いったいどういう処理が実施されてるんだろう。それ理解するのが今回の趣旨。

our-defmacro がすること

our-defmacro はマクロなので、(our-defmacro nil! (var) `(setq ,var nil)) は次のように展開される。

(progn
  (setf (our-expander 'nil!)
        #'(lambda (#:G3123)
            (block nil!
                   (destructuring-bind (var) (cdr #:G3123)
                   `(setq ,var nil)))))
  'nil!)))
; #:G3123 が gensym で定義されたシンボルだとしておく

progn では2つの式を順番に評価している。1つ目が setf で2つ目が 'nil! だ。

2つ目の結果が progn の評価結果となるので、この式の戻り値はシンボル nil! となる。

1つ目の setf のところを見ていこう。our-expander マクロが登場してるのでこれも展開してみる。

  (setf (get 'nil! 'expander) #'(lambda (#:G3123) (...)) )

nil! シンボルの expander プロパティに lambda を設定している。この lambda は ANSI Common Lisp の仕様書で expansion function と定義されているものに該当するようだ。

expansion function は「マクロを展開する処理」を実装した関数である。マクロ評価時に expansion function がどうやって呼ばれるかを見ていこう。

expansion function はどう呼ばれるか

expansion function を呼び出すのは our-macroexpand-1 だ。こいつを見ていく。

(defun our-macroexpand-1 (expr)
  (if (and (consp expr) (our-expander (car expr)))
      (funcall (our-expander (car expr)) expr)
      expr))

この関数は (our-macro-expand '(nil! x)) のように呼び出す。

1行目の if では、引数のチェックをしている。

  1. exprcons であるか?
  2. car expr(つまり 'nil!)に expand プロパティが定義されているか?

'(nil! x) はこの条件を満たすので、晴れて2行目が評価される。

2行目は、(funcall [expansion function] expr) を実施している。ここで expansion function が呼ばれている。引数は expr だ。

つまり、expansion function は '(nil! x) を引数に呼び出されることが分かった。

expansion function の定義を見つつ、our-macroexpand-1 の結果を追う

引数が分かったところで、expansion function の動きを追っていく。

expansion function を再掲しておく。この関数の引数が '(nil! x) である。

  #'(lambda (expr)
      (block nil!
             (destructuring-bind (var) (cdr expr)
             `(setq ,var nil)))))

(block nil! ...) では nil! という名前のブロックを定義してる。この構文のおかげで、マクロの中で return-from nil! を実行できるわけだ。

次の、destructuring-bindvar に引数の cdr を代入してる。ただの代入じゃなくて、構造化代入を使ってるが、詳しくは、On Lisp の 18. 構造化代入 に書いてあるのでここでは触れない。

ということで、ブロックの中を整理すると、次のような評価になる。

  (destructuring-bind (var) (x) `(setq ,var nil))

シングルクォートで書き直すとこうなる。

  (destructuring-bind (var) (x) (list 'setq var 'nil))

さらに let で書き直すとこうなる。

  (let ((var x))
    (list 'setq var 'nil))

つまり、expansion function の評価結果は

(setq x nil)

である。

おわりに

マクロの説明なのにマクロを使ってるのが少し気持ち悪いが、処理系内で何が起こってるかを説明するためにはこのような書き方になるんだと思う。

実際に処理系がマクロを評価する場合、our-macroexpand-1 で展開した結果のリストを再度評価する。つまり、(setq x nil) を評価して、実際に xnil が代入される。もし、マクロの評価結果のリストがマクロなら、もう一度、上記のような処理を実施するわけだ。

ちなみに、our-expander にあたる関数は Common Lisp では macro-function が用意されている。こいつにシンボル名を渡せば expansion function を取得できる。

On Lisp

On Lisp