【小ネタ】 簡易な関数定義マクロ, マクロ定義マクロを自作してみる

defunまでマクロで出来ているということはユーザがその気になれば関数定義の構文に手を入れたりも出来るということだ。 ~中略~ このような設計はマクロで制御構造までも作れるから出来るのだ。関数定義構文をマクロで定義している言語はLisp以外に私は知らない。

マクロについて整理してみる | κeenのHappy Hacκing Blog

とあるように、多くの言語において言語の基本要素である関数定義構文すらマクロで作れてしまうという点は、Lisp の特異でありまた面白い点です。そんな訳で、ごく簡易なものであればほんの数行で作れてしまう、という所を実際の Common Lisp のコードで見てみたいと思います。

なお、記事中の出力例は Clozure CL 1.11.5 を利用しています。



my-defun

まずは、結局のところ defun による名前付き関数の定義とは何をしているのかを見てみます。

とりあえず関数を一個定義してみます。

CL-USER> (defun hoge (a b)
           (+ a b))
HOGE
CL-USER> (hoge 1 2)
3

少々天下り式になりますが、このとき hoge シンボルの関数領域に「関数の実体」がひもづけられます。これは symbol-function によって確認することができます。

CL-USER> (symbol-function 'hoge)
#<Compiled-function HOGE #x3020045173AF>
CL-USER> (funcall #<Compiled-function HOGE #x3020045173AF> 1 2)
3

いきなり結論になりますが、この「シンボルの関数領域への関数のひもづけ」が「名前つき関数の定義」という操作の本質的な部分です。実際、シンボルの関数領域へ直接無名関数をひもづけてみると、同シンボルを関数として扱うことができてしまいます。

CL-USER> (setf (symbol-function 'hoge2)
               (lambda (a b)
                 (- a b)))
#<Anonymous Function #x3020045F404F>
CL-USER> (hoge2 1 2)
-1

ということで、「シンボルの関数領域へ無名関数をひもづける」操作をマクロ化した my-defun を作ってみます。

CL-USER> (defmacro my-defun (name args &body body)
           `(setf (symbol-function ',name)
                  (lambda ,args
                    ,@body)))
MY-DEFUN

使ってみます。できていますね。

CL-USER> (my-defun hoge3 (a b)
           (* a b))
#<Anonymous Function #x30200460884F>
CL-USER> (hoge3 2 3)
6

もちろん、これは実際のdefun に比べると全然機能が足りません。例えば、block を置いていないので、関数の途中で return-from することができません。が、そのぐらいであれば簡単に my-defun へ追加できます。

CL-USER> (defmacro my-defun (name args &body body)
           `(setf (symbol-function ',name)
                  (lambda ,args
                    (block ,name ; ← これ
                      ,@body))))
MY-DEFUN

return-from してみます。

CL-USER> (my-defun hoge4 ()
           (return-from hoge4 100)
           0)
#<Anonymous Function #x3020045E972F>
CL-USER> (hoge4)
100

ついでに、一々 (return-from <関数名>) と書くの面倒臭い!と思ったと仮定して、関数名なしでreturnできる my-return を使えるようにしてみます。

CL-USER> (defmacro my-defun (name args &body body)
           `(setf (symbol-function ',name)
                  (lambda ,args
                    (block ,name
                      (macrolet ((my-return (&optional return-value) ; ← これ
                                   `(return-from ,',name ,return-value)))
                        ,@body)))))
MY-DEFUN

my-return してみます。

CL-USER> (my-defun hoge5 ()
           (my-return 100)
           0)
#<Anonymous Function #x30200462C7AF>
CL-USER> (hoge5)
100

こんな感じで、シンプルな操作から始めて、自分で必要十分なところまで自作関数定義マクロを定義できてしまうというのは実に面白いところです。

my-defmacro

この辺から少々気味が悪くなるような話です。

defun はマクロとして定義されていたので、自身で再定義することができました。では、マクロを定義するのに利用した defmacro が何者かというと...CLHS に「Macro DEFMACRO」と書かれています。そう、defmacro もマクロです。つまり、自身で再定義することができます。

さて、関数定義と同じように、(名前付きの)マクロを定義するとシンボルのマクロ関数領域に(特定の形式の)関数がひもづけられます。これは、macro-function によって確認できます。

CL-USER> (macro-function 'my-defun)
#<Compiled-function MY-DEFUN Macroexpander #x30200453175F>

これまた my-defun で見たのと同様に、マクロ関数領域に自身で関数をひもづけることができます。

CL-USER> (setf (macro-function 'my-defun2)
               (lambda (param env)
                 (declare (ignore env))
                 (destructuring-bind (name args &body body) (cdr param)
                   `(setf (symbol-function ',name)
                          (lambda ,args
                            ,@body)))))
#<Anonymous Function #x30200463142F>

ここでは2つの引数をとる関数をひもづけています。第1引数 param は先頭にマクロ自身の名前、それ以降((cdr param))にいわゆるマクロの引数が入っています。なので、引数を destructuring-bind で取り出しています。第2引数 env はレキシカルな環境の情報が入っていますが、ややこしいので無視します*1

使ってみます。さすがにSLIMEの構文解析が追いつかないのでインデントはおかしくなっていますが動いています。

CL-USER> (my-defun2 hoge6 (a b)
                    (/ a b))
#<Anonymous Function #x3020046254BF>
CL-USER> (hoge6 1 2)
1/2

ここでまた my-defun と同じように...といきたいところですが、defmacro を利用するのは本末転倒です。いったん立ち止まって、ここまでで何が分かったのかを考えてみると、次のようになります。

「任意の」マクロを定義する = macro-function 領域に「任意の」コードを出力する関数をひもづける

この「任意の」は本当に任意です。つまりネストすることもできます。この左辺がやりたいことです。

「任意のマクロを定義する」マクロを定義する
  = macro-function 領域に
   「macro-function 領域に任意のコードを出力する関数をひもづける」
   コードを出力する関数をひもづける

右辺をじっくり見ていると頭が痛くなりそうですが、深く考えずにコードの方もネストさせてみます。

CL-USER> (setf (macro-function 'my-defmacro)
               (lambda (param env)
                 (declare (ignore env))
                 (destructuring-bind (name lambda-list &body body) (cdr param)
                   `(setf (macro-function ',name)
                          (lambda (param env)
                            (declare (ignore env))
                            (destructuring-bind ,lambda-list (cdr param)
                              ,@body))))))
#<Anonymous Function #x30200464CD9F>

この my-defmacro を利用して改めて関数定義マクロを定義します。

CL-USER> (my-defmacro my-defun3 (name args &body body)
                      `(setf (symbol-function ',name)
                             (lambda ,args
                               ,@body)))
#<Anonymous Function #x3020045F928F>

確かに動いています。

CL-USER> (my-defun3 hoge7 (a b)
                    (+ a a b b))
#<Anonymous Function #x3020045E580F>
CL-USER> (hoge7 1 2)
6

そんな訳で、マクロを定義するマクロを自身で再定義することができました。マクロの実用的な面白さはどんなマクロを書けるのかという方向にある訳ですが、ときには逆方向を眺めてみるのも面白いですね。


*1:扱おうと思ったら、param 内の &environment を自前で解析して env を束縛してあげればできそうな気はします...が、試していません