リードマクロ

LispのS式の生涯で重要な瞬間は3つ,すなわち読み込み時,コンパイル時,実行時だ. 関数を操れるのは実行時だ. マクロによりプログラムをコンパイル時に変換する機会が得られる. この章ではリードマクロを扱うが,これは読み込み時に機能するものだ.

マクロ文字

Lispの基本的思想に従い,リーダは大幅に制御することができる. リーダの動作は完全に実行中の変更が効く属性値と変数で制御できる. リーダは複数の段階でプログラムできる. 一番容易に動作を変更する方法は,新しいマクロ文字を定義することだ.

マクロ文字とはLispのリーダから特別な扱いを要求する文字だ. 例えば小文字のaは普通は小文字のbと全く同じ扱いだが,開き括弧は少々異なる. 開き括弧はリストの読み込みが開始したことの印だ. そういった文字にはそれぞれ関数が関連付けられており, それがLispのリーダにその文字を読み込んだ際の指示を与える. 既存のマクロ文字に関連付けられた関数を変更することもできるし, 独自のマクロ文字を定義することもできる.

組み込み関数set-macro-characterを使うのがリードマクロを定義する方法の1つだ. これは引数に1個の文字と1個の関数を取るが, その後readがその文字を読み込んだら,readはその関数を呼んだ結果を返すようになる.

(set-macro-character #\'
  #'(lambda (stream char)
      (list 'quote (read stream t nil t))))
\caption{'はこのようにも定義できる.} \label{fig:PossibleDefQuote}

Lispで最古参のリードマクロの1つは'すなわちクォートだ. 'がなくとも,常に'aの代わりに(quote a)とすることもできる. しかしそれは面倒だし,コードも読み辛くなってしまうだろう. 読み込みマクロquoteのおかげで'a(quote a)の代わりに使える. それは第\ref{fig:PossibleDefQuote}図のように定義することもできる.

readは通常のコンテキスト (例えば"a'b"|a'b|は除く)で'を読み込むと, 現在のストリームと文字についてこの関数を呼んだ結果を返す. (この関数は第2引数を無視する.どうせ必ずクォート文字だからだ.) よってread'aを読むと(quote a)を返す.

readの引数の最後の3個はそれぞれ以下のことを制御する. すなわち,ファイル終端に達したときにエラーを起こすべきか,起こさないなら何の値を返すか, readを呼び出している間にもreadが呼び出されるかどうか. ほとんど全てのリードマクロで第2と第4引数はtであるべきで, またその帰結として第3引数はどうでもよい.

リードマクロと普通のマクロには,共に関数が背後にある. そしてマクロの展開形を生成する関数と同様に,マクロ文字に関連付けられた関数は, 読み込み元のストリームに対して以外は副作用を持つべきでない. リードマクロに関連付けられた関数がいつ,またはどれ程頻繁に呼ばれるかについて, Common Lispは一切の保証を明示していない. (CLtL2の543ページを参照)

マクロとリードマクロは,コードをそれぞれ異なった段階で受け取る. マクロはコードがリーダによって既にLispのオブジェクトへとパースされた時点で受け取るが, リードマクロはコードがまだテキストである間にそれに作用する. しかしこのテキストに対しreadを呼ぶことで, リードマクロも(望みとあらば)同様にパース済みのLispオブジェクトを得ることができる. だからリードマクロは少なくとも普通のマクロと同程度の力を持っている.

実際には,リードマクロの方が少なくとも2点でマクロより強力だ. リードマクロはLispの読み込むもの全てに影響できるが,マクロはコード内に展開されるだけだ. またリードマクロは一般にreadを再帰的に呼ぶので,次のような式は,

''a

次のように展開される.

(quote (quote a))

しかしquoteの省略形を普通のマクロで定義しようとすると, それは孤立した状態では機能するが,

> (eq 'a (q a))
T

入れ子になると機能しない. 例えば,

(q (q a))

としても次のようにしかならない.

(quote (q a))

マクロ文字のディスパッチング

シャープクォートは,#で始まる他のリードマクロと同様, ディスパッチング・リードマクロと呼ばれる亜種の例だ. それらは2文字で使われるが,その1文字目はディスパッチング文字と呼ばれる. そのようなリードマクロの目的は,単にASCII文字集合を最大限に活用することだけだ. 1文字のリードマクロはせいぜいASCII文字集合の数だけしか定義できない.

make-dispatch-macro-characterによって)独自のディスパッチング・マクロ文字が定義できるが, #が既にそれとして定義されているので,それを使ってもよい. #で始まる組合せの幾つかは独自利用のために明示的に予約されている. 他の組合せは,Common Lispで予め定義された意味を持たない限り利用できる. 網羅的な一覧はCLtL2の531ページにある.

(defmacro q (obj)
  `(quote ,obj))
     (set-dispatch-macro-character #\# #\?
       #'(lambda (stream char1 char2)
            `#'(lambda (&rest ,(gensym))
                  ,(read stream t nil t))))
\caption{定数関数のためのリードマクロ.} \label{fig:ReadMacroForConstFunc}

ディスパッチングマクロ文字の新たな組合せは,関数set-dispatch-macro-characterで定義できる. これはset-macro-characterと似ているが,文字の引数を2個取る点が異なる. プログラマに予約されている組合せの一つは#?だ. 第\ref{fig:ReadMacroForConstFunc}図には, この組合せを定数関数のためのリードマクロとして定義する方法を示した. それによると#?2は任意個数の引数を取って2を返す関数として読み込まれる. 例:

> (mapcar #?2 '(a b c))
(2 2 2)

この例では新オペレータはかなり無意味に見えるが, 関数を引数に使うことがひどく多いプログラムでは,定数関数もしばしば必要になる. 事実,Lispの方言によっては定数関数を定義するためのalwaysという組込み関数が用意されている.

マクロ文字をそのマクロ文字の定義内に使うことは全く問題ないことに注意しよう. 他のどのLisp式とも同じように,それらは定義が読み込まれたときに消えてしまう. マクロ文字は#?の後に使っても構わない. #?の定義ではreadを呼んでいるので, '#'等のマクロ文字は普通通りに機能する.

> (eq (funcall #?'a) 'a)
T
> (eq (funcall #?#'oddp) (symbol-function 'oddp))
T

デリミタ

(set-macro-character #\] (get-macro-character #\)))

(set-dispatch-macro-character #\# #\[
  #'(lambda (stream char1 char2)
      (let ((accum nil)
            (pair (read-delimited-list #\] stream t)))
        (do ((i (ceiling (car pair)) (1+ i)))
          ((> i (floor (cadr pair)))
           (list 'quote (nreverse accum)))
          (push i accum)))))
\caption{リードマクロを定義するデリミタ.} \label{fig:ReadMacroDefngDlmt}

単純なマクロ文字の次によく使われるマクロ文字は,リストのデリミタだ. ユーザに予約されている組合せには#[もある. 第\ref{fig:ReadMacroDefngDlmt}図には, それを手の込んだ仕組みを持つ開き括弧の一種として定義する方法を示した. そこでは#[x y]という形の式を x以上y以下の整数全てから成るリストとして読み込ませるようにしている.

> #[2 7]
(2 3 4 5 6 7)

このリードマクロで新しいのはread-delimited-listを呼んでいる点だけだ. これはちょうどこのような場合のために用意された組込み関数だ. 第1引数にはリストの終わりとして扱う文字を与える. ]#[を閉じるデリミタとして認識されるためには, 先にデリミタの一種に含まれていなければならないので,予めset-macro-characterを呼ぶ必要がある.

(defmacro defdelim (left right parms &body body)
  `(ddfn ,left ,right #'(lambda ,parms ,@body)))

(let ((rpar (get-macro-character #\) )))
  (defun ddfn (left right fn)
    (set-macro-character right rpar)
    (set-dispatch-macro-character #\# left
       #'(lambda (stream char1 char2)
           (apply fn
                  (read-delimited-list right stream t))))))
\caption{デリミタ・リードマクロを定義するためのマクロ.} \label{fig:MacroForDefngDlmtReadMacro}

デリミタ・リードマクロの定義ではほぼ毎回第\ref{fig:ReadMacroDefngDlmt}図のコードの多くが 繰り返し使われるだろう. マクロならそれを行う仕組みに抽象化されたインタフェイスを備えさせることができる. 第\ref{fig:MacroForDefngDlmtReadMacro}図には, デリミタ・リードマクロを定義するためのユーティリティの定義例を示した. マクロdefdelimは引数に2個の文字,仮引数リスト及び本体になるコードを取る. 引数リストと本体コードは暗黙のうちに関数を定義する. defdelimを呼び出すと,1文字目をディスパッチング・リードマクロとして, 2文字目をその終端として定義する. そして定義された関数をそれらの間のものに適用した結果を返すようにする. ついでに言えば,第\ref{fig:ReadMacroDefngDlmt}図の関数本体もユーティリティを欲しがっている. すなわち,既に定義したmapa-b(ckvページ)だ. defdelimmapa-bを使うと, 第\ref{fig:ReadMacroDefngDlmt}図で定義したリードマクロは次のように書ける.

(defdelim #\[ #\] (x y)
  (list 'quote (mapa-b #'identity (ceiling x) (floor y))))

デリミタ・リードマクロは関数の合成にも使うと便利だ. 第5.4節では関数合成用のオペレータを定義した.

> (let ((f1 (compose #'list #'1+))
        (f2 #'(lambda (x) (list (1+ x)))))
    (equal (funcall f1 7) (funcall f2 7)))
T

組込み関数のlist1+等を合成するときには, composeの呼び出しの評価を実行時まで待つ理由はない. 第5.7節では代替方法を紹介した. リードマクロのシャープドットをcompose式の頭に付けることで,それを読み込み時に評価できる.

#.(compose #'list #'1+)
(defdelim #\@{ #\@} (&rest args)
          `(fn (compose ,@args)))
\caption{関数合成のためのリードマクロ.} \label{fig:ReadMacroForFuncComp}

ここでは似ているがさらにきれいな解決法を示そう. 第\ref{fig:ReadMacroForFuncComp}図のリードマクロは,#@{f 1 f 2 ...fn@}という形の式が f1f2,...,fnを合成したものとして読み込まれるように定義する. よってこうなる.

> (funcall #@{list 1+@} 7)
(8)

これはfn(gfdページ)の呼び出しを生成することで動作する. fnはコンパイル時に関数を生成できるものだ.

いつ何が起きるのか

最後に,混乱するかもしれない点についてはっきりさせておいた方がよいかもしれない. リードマクロが普通のマクロより先に呼び出されるなら, マクロがリードマクロを含む式に展開されるというのは何なのだろうか? 例えば次のマクロは,

(defmacro quotable ()
  '(list 'able))

クォートを含む式に展開される. さてそれは本当だろうか? 実は,このマクロの定義内にあるどちらのクォートもdefmacro式が読み込まれたときに展開され, 次のようになる.

(defmacro quotable ()
  (quote (list (quote able))))

普通はマクロの展開形にリードマクロが含まれると思っていても不都合はないが, それはリードマクロの定義は読み込み時とコンパイル時で変わらないはず(変わるべきでない)からだ.


←: マクロを定義するマクロ     ↑: On Lisp     →: 構造化代入

Copyright (c) 2003-2011 野田 開     NODA Kai <nodakai@gmail.com>