マクロについて整理してみる

何故Lisperがマクロについて語るのか。Lisperと議論してみても満足のいく答を得た人はそんなにいないと思う。 それはLisper自身便利とは思っていても何が便利なのかを意識してなくて他人に上手く説明出来ないからじゃないかと思った。 ちょっと思いついた範囲でまとめてみる。 最近ではマクロシステムを持つ言語は珍しくない。Rust, Scala, Template Haskell, Mirahなどなど。最初にCommon Lispのマクロと他の言語のマクロとの違いを少し考えてみたい。

Unless

まず、unlessをマクロで書くことを考えてみたい。単純にifにnotをつければいい。

Common Lispではこうなる。

(if (not foo-p)
    bar)

(defmacro unless (cond then)
  `(if (not ,cond)
       ,then))

(unless foo-p
  bar)

Rustだとこうなるだろうか。


if ! isFoo {
    bar;
}

macro_rules! unless {
    ($cond:expr, $then:stmt) => {{
        if ! $cond {
            $then
        }
    }};
}

unless!(isFoo, {
    bar;
})

あるいはmirahだとこうなる。unlessが予約語なので_unlessにした。

if ! isFoo
  bar
end

macro def _unless(cond, block:Block)
  quote{
    if ! `cond`
      `block.body`
    end
  }
end

_unless isFoo do
  bar
end

ちなみにCだとこう出来る。

if(! is_foo)
  bar;

#define unless(cond) if(!(cond))

unless(is_foo)
  bar;

さて、上のマクロたちを見比べて欲しい。Common LispのマクロとCのマクロは違和感なく新たな制御構造を作れたのに対してRustとMirahはマクロ呼び出しが剥き出しになっている。

ここで注意して欲しいのはCはともかくCommon Lisp、Rust、Mirahのマクロは全て引数に必要な構文木をとってそのまま用意したテンプレートにあてはめているだけだ。やっていることに違いはない。 何が違うかというとマクロ呼び出しの構文が制御構造の構文と同じか違うかという点だけだ。私の知る範囲ではそのような構文をした言語はLispしかない。だから「Lispの」マクロなのだ。

もう一つ。どのマクロ定義も十分直感的に読めたと思う。この範囲なら S式だからマクロが簡単になるということはない と思う。別にS式でなくてもquasiquoteがあれば十分読み易いマクロが書ける。

Case

caseを生成することを考えよう。

(defun fun1 (x)
  (let ((y (case x
             ((:foo) (hoge "foo"))
             ((:bar) (hoge "bar!")))))
    ...))


(defun fun2 (x)
 (let ((y (case x
            ((:foo) (hoge "foo"))
            ((:baz) (hoge "baz!")))))
   ...))

のように似たような、しかし微妙に違うcaseが必要になったような時に必要だろう。この時、'(:foo (hoge "foo") :bar (hoge "bar!"))のようにリテラルでそのまま書いてしまっては抽象化する意味がなくなる。例えばハッシュテーブルにKey-Valueペアを入れて必要な時に入れたり削除したりすればよい。そのようなマクロはこう書ける。

(defmacro case-table (key hash)
  `(case ,key
     ,@(loop
          :for k :being :the :hash-key :of hash :using (hash-value v)
          :collect `((,k) ,v))))

(defparameter *table*)
(setf (gethash :foo *table*) '(hoge "foo"))
(setf (gethash :bar *table*) '(hoge "bar!"))

(defun fun1 (x)
  (let ((y (case-table x #.*table*)))
    ...))

(remhash :bar *table*)
(setf (gethash :baz '(hoge "baz!")))


(defun fun2 (x)
  (let ((y (case-table x #.*table*)))
    ...))

ハッシュテーブルを受け取ってリストを返す普通のloopマクロの力をマクロ展開時にも使える。また、ハッシュテーブルには'(hoge "foo")などのただのリストを突っ込んだがそれをそのままASTとしても使っている。 Common Lispのマクロのもう1つの良いところはマクロの展開時にもCommon Lispの機能がフルで使えるところであり、マクロの引数に受け取るのが構文木でありデータ構造でもあるS式なところだ。ここでは S式の同図像性が重要なファクターになっている

(case-table x #.*table*) で使っている #.リードマクロ といってマクロ展開(コンパイル時)より前(リード時、構文解析時)に動作するマクロだ。このリードマクロはeval-when-readといってリード時に 値を評価する。その結果、case-tableに渡るのが '*table*というシンボルでなく*table*に束縛されているハッシュテーブルになる。因みに コンパイラマクロ というマクロ展開が終わった後に動作するマクロもある。

マクロ1つではなくマクロの前後に動作するマクロもあってこそ初めて柔軟性を手にしている。あまりその点は議論されてないのではないだろうか。

Common Lispの設計

さて、各々言語には予約語や~構文というものがあると思う。メソッド定義構文など。Common Lispにもスペシャルフォームという名前で25個存在する。 驚くべきことに、ここまでに出てきたCommon Lispのコードにはスペシャルフォームは3つしか出てこない。ifとletとquoteだ。setfやloop、defun、defmacro、case、defparameterなんかはマクロで出来ている。

defunまでマクロで出来ているということはユーザがその気になれば関数定義の構文に手を入れたりも出来るということだ。 普通、そんなことをしたら阿鼻叫喚だがCommon Lispなら安全に出来る。マクロもシンボルに束縛されているので新たにマクロで上書きしたシンボルを名前空間にインポートしなければ今まで通りのdefunやloopが使える。

もう1つ、普段使うものがマクロで出来ているということはマクロを被せてない機能は相当低レベルだということだ。マクロさえ書けば全く新しい言語機能を追加出来るということであり、あるいは全く新しい言語も作れてしまう

このような設計はマクロで制御構造までも作れるから出来るのだ。関数定義構文をマクロで定義している言語はLisp以外に私は知らない。

メタプログラミングとCommon Lisp

ところで、マクロに限らないメタプログラミングについて考えてみたい。メタプログラミングとは、プログラムを書くプログラムを書くことだ。プログラムはテキスト形式のソースコードから実行形式に至るまでの間に様々な中間形式から中間形式に変換される。メタプログラミングはそのどこかの形式を生成することと同義になる。通常、処理系内部で使う形式は変なことをされると困るのであまり外部に公開したくない。メタプログラミングを許すとしても構文解析などのフロントエンドに近い部分になるだろう。

例えばyaccやlexのようなソースコードジェネレータ、rubyやDのような文字列ベースのメタプログラミングは公開APIであるところのパーサだけを使っているので安全性は高い。しかし文字列だけだと構造がないので書く側はつらい。一応、Common Lispにもread関数とeval関数があるのでCommon Lispのソースコード文字列を吐いてreadしてevalすれば文字列メタプログラミングは出来る。特に嬉しくないのでやらないが。

次はトークンベースのメタプログラミングがある。Cのプリプロセッサなど。Common Lispでもリードマクロで可能だ。例えば正規表現リテラルを作ったりとかの例がある。

次はASTベースのメタプログラミングがある。マクロだ。これは今まで見てきた。

これより先はメタプログラミングとはあまり言わない気がする。JITと呼ぶのではないか。Common Lispにもeval関数やcompile関数があるからそういうことも出来る。正規表現ライブラリなんかが使っている。

私が言いたいのはCommon Lispにはメタプログラミングの機構が一杯あるということではない。read, eval, compile…。気付いたかもしれないが、LispはLisp自身で出来ている。 Lispを書くとき、Lispを書いてるかもしれないしLispを構成するLispを書いているかもしれない。いくらでもメタプログラミングが出来るということだ。

まとめ

何やらCommon Lisp賛美歌になってしまったが一応まとめておく。

  • 簡単なマクロ定義ならS式である必要はない
  • 複雑なマクロ定義ならS式であると書き易い。恐らく他の構文でも書けないことはない。
  • マクロ呼び出し構文はS式でないと重大な違いがある。
  • マクロ以外にもリードマクロやパッケージなど他の言語機能があってこそマクロが活きる。
  • 言語機能だけでなくマクロを前提とした設計も重要である。
  • LispはS式で出来ている以前にLispで出来ている。
Written by κeen