やさしいLispマクロの使い方


はじめに

Lispの書籍の中には極めて高度なマクロテクニックを駆使したコードが見受けられます。私にはもう理解不能なほどに高度なテクニックが駆使されています。Lisp初心者向けにやさしい例はないだろうか、と自分なりに書いてみました。ISLispに拠っています。

マクロが欲しくなる時

ISLisp規格にはforやwhileといった最低限の制御構造のための構文が用意されています。例えば次のように書くことができます。


(defun foo (n)
  (while (< n 10)
         (format (standard-output) "~A " n)
         (setq n (+ n 1))))

> (foo 0)
0 1 2 3 4 5 6 7 8 9 NIL
> 

ところで、whileのように最初に条件判定をするのではなく、Pascalのrepeat/untilのように判定を最後で行いたい場合にはどうしたらいいでしょうか?こんな風に書きたいのです。



(defun bar (n)
  (repeat
    (format (standard-output) "~A " n)
    (setq n (+ n 1))
    (until (= n 10))))

RubyやPythonではユーザーが自由に制御構文を設定することはできません。Rubyならまつもとさんにお願いして新たな構文を追加してもらうほかありません。しかし、Lispの場合には自分の欲しい制御構文を任意に追加することができます。マクロと呼ばれているものです。

期待する展開

Lispではマクロが与えられると直ちに所定の表現に置き換えられます。上記のrepeat/untilの構文はどういったものに置き換えられればいいのでしょうか。いくつか考えられますが、ISLispに標準で備わっているblock、return-from、tagbody、goを使って同等なコードを考えてみます。


(defun bar (n)
  (block exit
    (tagbody
      loop
      (format (standard-output) "~A " n)
      (setq n (+ n 1))
      (cond ((= n 10)(return-from exit t)))
      (go loop))))

さあ、うまく動作するでしょうか?


> (bar 0)
0 1 2 3 4 5 6 7 8 9 T
> 

どうやらよさそうです。

マクロを考える

repeatについて考えてみます。repeatは任意個のS式を受け取ってblockやtagbodyを使った構文に変換すればいいはずです。


(defmacro repeat (:rest expr)
  `(block exit
      (tagbody
        loop
        ,@expr
        (go loop))))

「`」という記号が使われています。バッククォートといいます。この記号を付けられたS式は準クオートと呼ばれています。展開されてほしいひな形を意味しています。引数には:restというキーワードが付けられています。repeatは任意個のS式を受け取ります。それらをまとめてリストにして受け取りたいのでこうしています。(repeat a b c)とすると引数であるexprには(a b c)が入ります。この受け取ったものをblockの構文に埋め込みます。,@exprとなっている部分に埋め込みます。「,」記号が使われています。アンクォートと呼ばれています。この記号を付けておきますと引数exprが受け取った値に置き換えられます。ところで,@のように「@」記号が使われています。これがくっつくとアンクォート・スプライシングという呼ばれるものになります。埋め込むときに括弧を1つ取り外して埋め込みます。こうしてやらないと構想したS式にうまくなってくれません。

うまくできたかどうかを試してみます。maceoexpand-1という関数があり、これを利用するとうまく展開できるかどうかを確かめることができます。


> (macroexpand-1 '(repeat (boo 1)(uoo 2)))
(BLOCK EXIT (TAGBODY LOOP (BOO 1) (UOO 2) (GO LOOP)))
> 

どうやらうまくいったようです。

次はuntilのマクロです。untilはcond節に変換してreturn-fromで脱出できるようにしたいです。こんな風になろうかと思います。


(defmacro until (expr)
  `(cond (,expr (return-from exit t))))

repeatのときと違って:restは使いません。untilが受け取るのは1個のS式だけだからです。そしてそれをcond節に埋め込みます。今度は@が付かないただのアンクォートです。untilが受け取った条件式であるS式を埋め込みます。

うまくいくでしょうか? macroexpand-1を使って確認します。


> (macroexpand-1 '(until (= n 10)))
(COND ((= N 10) (RETURN-FROM EXIT T)))
> 

うまくいっているようです。

さあ、これでうまくrepeat/until構文が動作するはずなのですけど、どうでしょう。


(defun bar (n)
  (repeat
    (format (standard-output) "~A " n)
    (setq n (+ n 1))
    (until (= n 10))))

> (bar 0)
0 1 2 3 4 5 6 7 8 9 T
> 

やりました!成功です。

処理系内部の裏事情

バッククォート記号が付いた準クォートは読み込まれると直ちに次のS式に変換されます。例えば `(a b c)です。


`(a b c)

(cons 'a (cons 'b (cons 'c nil)))

「,」のアンクォートが付けられますとちょっと変わります。例えば `(a ,b c) です。


`(a ,b c)

(cons 'a (cons b (cons 'c nil)))

bだけはクォート記号が付いていません。ですからbに束縛された値に置き換えられることとなります。

,@のアンクォート・スプライシングはconsではなくappendに変換されます。


`(a ,@b c)

(cons 'a (append b (cons 'c nil)))

マクロを使う場面

このようにマクロを使うと新たな制御構造を作り出すことができます。どんなときに使ったらいいのでしょうか?

M.Hiroiさんという方が書かれる入門解説文書はとてもわかりやすいのですが、私はHiroiさんのページで見つけた次の言葉に感銘を受けました。

M.Hiroiさんのページから引用します。
http://www.geocities.jp/m_hiroi/xyzzy_lisp/abclisp11.html

引用開始
Common Lisp に tagbody と go が用意されているのは、基本的な繰り返しや制御構造をマクロで実現するためです。Common Lisp には便利なマクロが多数用意されているので、一般的なプログラムであれば tagbody と go を使う必要はまったくありません。go の使用について CLtL2 (参考文献 [3]) より引用します。
『スタイルの問題として、go を用いる前に二度考えることを勧める。go のほとんどの目的は、繰り返しのための基本構文のうちの1つ、入れ子になった条件フォーム、あるいは return-from を用いて達成することができる。もし go の使用が避けられないと思われるならば、おそらく go によって実現される制御構造は、マクロ定義としてパッケージ化されるべきである。』
tagbody と go を安易に使用してはいけません。くれぐれもご注意くださいませ。
引用終わり

終わりに

私はあまりマクロは得意ではありません。なので関数で書けるものは関数で書いてしまいます。マクロを使うことはあまりありません。しかし、場面によってはマクロを使うとコードが整理され、意図するところが明確になる場合もあり、そのときには利用しています。

私の駄文がなにかしらLisp入門のお役に立ちましたら幸いです。

コマーシャル

ISLispによるやさしいLispの入門書をKindleから出版しております。よかったらお読みください。
https://www.amazon.co.jp/dp/B074HWYR5N