- マクロは「コードを受け取ってコードを返す」仕組みで、関数ではできない、評価タイミングを制御したり、式のテキストを取得したりすることができます。
defmacroでマクロを定義し、バッククォート(`)とカンマ(,)でテンプレートとして展開コードを組み立てるのが一般的な作り方です。- マクロの展開結果は、
macroexpand-1で確認できます。
1. マクロでできて関数ではできないこと
プログラムを書いていると、「コードそのものを扱いたい」場面というのがたまにあります。
たとえば、デバッグ・ログ。
式の中身を確認するために出力します。
(defun log-value (str x)
(format t "value ~a~: ~a~%" str x))
(defpameter n 12)
(log-value "n" n)
; value n: 12Code language: Lisp (lisp)
しかし、この log-value 関数では、メッセージ文字列 “n” と式 n で2回書くのが面倒です。
コードの式と評価結果を両方出力できれば、もっとスムーズです。
そういうときに、役立つのがマクロです。log-exprマクロを作ると、こういうコードが書けます。
(log-expr n)
; n => 12
(log-expr (* 3 4))
; (* 3 4) => 12Code language: Lisp (lisp)
与えた式を文字列にしてから、値が評価され出力しています。
「コードを受け取って後から評価したい」ときに、マクロが役に立つのです。
1.1. defmacro の基本(log-expr)
log-exprマクロは defmacro で定義します。
(defmacro log-expr (expr)
`(format t "~a => ~a~%" ',expr ,expr))Code language: Lisp (lisp)
マクロは、コードを生成して返し、そのまま評価されます。
バッククォート `( .. ) でコードの雛形を作り、 カンマ, が引数の値をそこに埋め込んでいます。
コードを文字列で使うときは、 ',expr のように取得します1。
(log-expr x)
;=> (FORMAT T "~a => ~a~%" 'x x)
; x => 12Code language: Lisp (lisp)
ちゃんと ,'expr には 'xが、 ,expr の位置に、 x が埋め込まれたコードに展開されました。
同じことは、関数ではできません。
(defun func-log-expr (expr)
(format t "~a~ => ~a~%" 'expr expr))
(func-log-expr x)
;=> expr => 12Code language: Lisp (lisp)
与えた実引数 x ではなく、関数内部の仮引数 expr がコードとして表示されます。
関数では、x の値を評価してから、 expr に束縛しているからです。
一方、マクロでは x というコードをそのまま受け取って、内部で評価します。
1.2. マクロはリストを返せばよい
マクロは引数を元にリストを返し、それをコードとして評価する仕組みです。
マクロの展開コードを組み立てるには、バッククォート(`)がよく使われますが、コードの雛形を作ることは、マクロの本質ではありません2。
コードとして評価できるなら 単なるリストでも構わないからです。
次は、シンボルと値をコンスセルにするマクロを作ってみます。
(defmacro cons-variable (v)
(list 'cons (list 'quote v) v))Code language: Lisp (lisp)
このマクロは、コンスセルを作ります。
(defparameter x 10)
(cons-variable x)
;-> (CONS 'X X) に展開され
;=> (X . 10) と評価されるCode language: Lisp (lisp)
この (list 'cons (list 'quote v) v) は、コードをリストとして作っていることがわかります。
クォートされたシンボルCONSやQUOTEはそのままで、リストが生成され、それがそのまま評価されています。
これが、「Lisp ではコードはリストである」ということの意味です。
ちなみに、関数でもコンスセルを作る前のリストが返せますが、関数が返すリストを eval で評価しても、必ずしも同じ結果にはなりません。
(defun f-cons-variable (v)
(list 'cons (list 'quote v) v))
(f-cons-variable x)
;=> (CONS '10 10)
(eval (f-cons-variable x))
;=> (10 . 10)Code language: Lisp (lisp)
やはり x は影も形もなくなります。
1.3. バッククォートとカンマ(リストの部分評価)
この cons-variable マクロは、バッククォートを使って、もっと簡潔に書けます。
(defmacro cons-variable (v)
`(cons ',v ,v))Code language: Lisp (lisp)
バッククォートを使うと、CONSやQUOTEに一つ一つクォートする必要がなくなります。
代わりに、評価が必要な部分にだけカンマがつけます。
このバッククォートの記法は、マクロ展開後のコードとの対応が見やすいために重宝します。
実は、バッククォートやカンマは、マクロ以外でも利用できます。
クォートとバッククォートでリストを作ると、部分評価の箇所に違いがあるだけで、ほとんどそっくりです。
;; クォートによるリスト生成
(let ((x 10))
(print '(value is x))) ;=> (VALUE IS X) ; x はシンボルのまま評価されない
;; バッククォートによるリスト生成
(let ((x 10))
(print `(value is ,x))) ;=> (VALUE IS 10) ; ,x が部分的に評価されてリストにCode language: Lisp (lisp)
',v という記法も、特殊なものではありません。
カンマ, の前に ' を付けるのは、評価した結果をクォートしているからです。
v を部分的に評価して束縛されているコードを得て、これをクォートすれば、実行時にデータとして扱えます。
2. ボディを展開するマクロ(可変長引数 &body)
次に、可変長引数 &body を取る while マクロを作ってみます。
defmacro の引数リストは defun と同じラムダリストを使えます。&optional・&rest・&key も使えますし、&body も使えます。
こんな感じで、条件式の後に実行コードを並べて書きます。
(defparameter n 0)
(while (< n 3)
(print n)
(incf n))
; 0
; 1
; 2Code language: Lisp (lisp)
whileは、loop のシンタックスシュガーとして使えます3。
(defmacro while (condition &body body)
`(loop
(unless ,condition (return))
,@body))Code language: Lisp (lisp)
&body は残りのフォームをリストとして受け取ります。,@body でそのリストを展開して loop の中に並べています。
while を関数ではなくマクロで作ったのは、評価タイミングの問題があるからです。
条件となる condition は loop が回るたびに評価されなければなりません。
2.1. リストを展開して連結する(,@)
whileマクロには、 ,@body という構文がありました。
,@ はリストを展開して連結します。
つまり、リストをそのまま埋め込む , と異なり、要素を一段フラットに展開します。
body が ((print n) (incf n)) というリストになっているとき、,@body でその中身を展開して loop の中に並べます。
これも、マクロ特有の記法というわけではなく、評価したリストがシンボル列に置き換わります。
(let ((items '(1 2 3)))
`(list ,items)) ;=> (LIST (1 2 3)) ; リストがそのまま入る
`(list ,@items)) ;=> (LIST 1 2 3) ; 展開されて入るCode language: Lisp (lisp)
2.2. &body の使い方
defmacro の引数リストで &body を使うと、残りのフォームをリストとして受け取ります。
動作は &rest と同じですが、&body にはエディタへのヒントという追加の意味があります。
SLIMEなどのエディタは &body を検出すると、その部分のインデントをフォームの先頭ではなく2スペース下げるのです。
(defmacro my-when (condition &body body)
`(if ,condition (progn ,@body)))
; &rest を使っても動作は同じ
(defmacro my-when (condition &rest body)
`(if ,condition (progn ,@body)))Code language: Lisp (lisp)
with-open-file や let のような「ボディを持つフォーム」らしいインデントになります。
; &body のインデント
(my-when (> x 0)
(print x) ; 2スペースインデント
(print "positive"))
; &rest だと関数と同じインデントになってしまう
(my-when (> x 0)
(print x)
(print "positive"))Code language: Lisp (lisp)
見た目の違いだけでなく、「これはボディを持つマクロだ」という意図を明示するためにも、ボディを受け取る場合は &rest より &body を選びます4。
3. マクロ展開を確認する(macroexpand-1)
ちなみに、マクロがどのように作用しているか、展開結果は確認できます。
macroexpand-1 は1段階だけ展開し、macroexpand はすべてのマクロが展開されるまで繰り返します。
(macroexpand-1 '(while (< n 5) (print n) (incf n)))
;=> (LOOP
; (UNLESS (< N 5) (RETURN))
; (PRINT N)
; (INCF N))
; TCode language: Lisp (lisp)
macroexpand-1 の戻り値は2つあります。
展開されたコードと、
実際に展開が行われたかを示すフラグです。
入れ子のマクロがある場合は macroexpand-1 を繰り返すか、macroexpand で一気に展開します。
展開結果を見るときのポイントは、
- 変数名が意図通りか、
,@の展開が正しく行われているか、- 余分な括弧がないかの3点です。
3.1. slime-macroexpand-1 でチェックできる
ちなみに、SLIMEでは C-c RET(slime-macroexpand-1)で、カーソル位置のマクロをその場で展開して確認できます。
マクロを書くときはこの操作を手癖にすると、展開結果がずれていてもすぐ気づけます5。
4. swapマクロと変数キャプチャ
マクロを使う代表的な例に、2つの変数を交換する swap があります。
先に動作例を見せます。
(defparameter x 1)
(defparameter y 2)
(swap x y)
x ;=> 2
y ;=> 1Code language: Lisp (lisp)
この swap は関数で書こうとしても、関数内に x と y の値がコピーされるだけで、元の変数は書き換えられません。
そこで、マクロの出番です。
(defmacro swap (a b)
`(let ((tmp ,a))
(setf ,a ,b)
(setf ,b tmp)))Code language: Lisp (lisp)
引数として渡されたコードが評価される前にマクロが展開されます。
マクロによって、どのようにコードが展開されるかは、macroexpand-1 で確認できます。
(macroexpand-1 '(swap x y))
;=> (LET ((TMP X))
; (SETF X Y)
; (SETF Y TMP))Code language: Lisp (lisp)
マクロは変数名そのものをコードとして受け取るので、setf で元の場所に書き込めます。
ただし、この実装のままでは欠陥があります。
それは、展開コード内で tmp という名前を使ったこと。
もし、呼び出し側に tmp という変数があると衝突してしまうのです。
4.1. マクロ内部変数と変数キャプチャ
マクロを書くとき、もっとも気をつけるべき罠が、「変数キャプチャ」です。
マクロが意図せずに外側のスコープの変数を「捕まえて」しまう現象です6。
たとえば、こんなマクロを書いたとします。
(defmacro my-dotimes (n &body body)
`(do ((i 0 (+ i 1)))
((= i ,n))
,@body))Code language: Lisp (lisp)
普通に使う分には問題なさそうです。
(my-dotimes 3 (print "hello"))
; "hello"
; "hello"
; "hello"Code language: Lisp (lisp)
しかし、呼び出し側に i という変数があると壊れます。
マクロ内部で使った変数名が、呼び出し側のスコープにある同名の変数に干渉するからです。
(let ((i 10))
(my-dotimes 3
(print i)))
; 0 ; ← i が呼び出し側の 10 ではなく、マクロ内部の i に差し替えられている
; 1
; 2Code language: Lisp (lisp)
呼び出し側では i が 10 であることを期待しているのに、マクロ内で i が上書きされ 0 になっています。
4.2. gensym で衝突しない変数名を作る
変数キャプチャを防ぐには、マクロ内部で使う変数のために、衝突しない名前を作ります。
それが gensym です。
gensym は呼ぶたびに一意のシンボルを生成します7。
(gensym) ;=> #:G123
(gensym) ;=> #:G124
(gensym) ;=> #:G125Code language: Lisp (lisp)
#:G123 のようなシンボルはパッケージに属さないため、ユーザーコードにある G123 とも衝突しません。
あるいは、gensymには、文字列を付けることもできます。
(gensym "TMP") ;=> #:TMP1968Code language: Lisp (lisp)
これを使って my-dotimes を書き直します。
(defmacro my-dotimes (n &body body)
(let ((i (gensym "I")))
`(do ((,i 0 (+ ,i 1)))
((= ,i ,n))
,@body)))Code language: Lisp (lisp)
gensym の呼び出しはバッククォートの外、マクロの展開コードを組み立てる前に let で割当てます。
そして、i を評価して、生成したシンボルを内部で使います。
(macroexpand-1 '(my-dotimes 3 (print "hello")))
;=> (DO ((#:I1969 0 (+ #:I1969 1))) ((= #:I1969 3)) (PRINT "hello"))Code language: Lisp (lisp)
i ではなく #:I1969 のような一意の名前になっているので、呼び出し側の変数と衝突しません。
先ほどのswapマクロは、以下のようにあらかじめ、tmpをgensymで生成したシンボルに束縛して、バッククォート内で部分評価します。
(defmacro swap (a b)
(let ((tmp (gensym "TMP")))
`(let ((,tmp ,a))
(setf ,a ,b)
(setf ,b ,tmp))))Code language: Lisp (lisp)
4.3. アナフォリックマクロ(意図的な変数キャプチャ)
アナフォリックマクロは、評価結果を it という名前に自動的に束縛します。aif は if のアナフォリック版です。
条件式の結果を、it という変数で参照できる設計になっています。
(defmacro aif (condition then &optional else)
`(let ((it ,condition))
(if it ,then ,else)))
(aif (find-if #'evenp '(1 3 5 6 7))
(format t "found: ~a~%" it)
(format t "not found~%"))
; found: 6Code language: Lisp (lisp)
it は、意図的に変数キャプチャを使っています。
通常の変数キャプチャは罠ですが、アナフォリックマクロはその性質を活用しています。
条件式の結果を it という名前で使えるようにしているのです。awhen・awhile・aand なども同じ発想で作れます8。
(defmacro awhen (condition &body body)
`(aif ,condition (progn ,@body)))
(awhen (assoc :name *player*)
(format t "player name: ~a~%" (cdr it)))Code language: Lisp (lisp)
5. 多重評価の罠
変数キャプチャと並んでよく起きる罠が、多重評価です。
incfなどマクロの引数に副作用のある式を渡すのには注意が必要です。
マクロがその式を展開コードに複数回埋め込んでいると、意図せず複数回評価されるからです。
たとえば、値を二乗するマクロを素朴に書くとこうなります。
(defmacro square (x)
`(* ,x ,x))Code language: Lisp (lisp)
数値を渡す分には問題ありません。
(square 5) ;=> 25Code language: Lisp (lisp)
しかし、副作用のある式を渡すと、式が2回評価されます。
(let ((count 0))
(square (progn (incf count) count)))
;=> 2 ; count が 1 になってから 2 になる。
1 * 1 ではなく 1 * 2Code language: Lisp (lisp)
(progn (incf count) count) が2回展開されているため、1回目の評価で count が 1 に、2回目の評価で 2 になり、1 * 2 = 2 という結果になっています。
5.1. 評価する前に let で束縛する
回避するには、引数を一度だけ評価して let で束縛してから使います。
(defmacro square (x)
(alexandria:with-gensyms (val)
`(let ((,val ,x))
(* ,val ,val))))Code language: Lisp (lisp)
こうすると x は一度だけ評価され、その結果が val に束縛されます。
(let ((count 0))
(square (progn (incf count) count)))
;=> 1 ; count が 1 になり、1 * 1 = 1Code language: Lisp (lisp)
引数を展開コード内で複数回使うマクロを書くときは、常に多重評価の可能性を疑います。gensym で束縛してから使う、というパターンを習慣にします9。
6. 【応用】マクロで受け取るコードはリスト
Lispのマクロは、コードの一部をパラメータで置き換えるだけではありません。
「引数をリストとして操作して、コードを作る」ということがわかると、Lispの面白さがわかります。
6.1. with-bindingsを作る
with-bindigsマクロを作ってみます。
このマクロは、複数のキーに値を束縛して式を評価します。
(with-bindings (:x 1 :y 2 :z 3)
(+ x y z))
; => 6
(with-bindings (:name "Alice" :age 30)
(format t "~a is ~a years old~%" name age))
; => Alice is 30 years oldCode language: Lisp (lisp)
このマクロ定義では、フラットなリスト pair (:x 1 :y 2 :z 3)から2個ずつ取り出し、letが受け取れる bindings ((x 1) (y 2) (z 3))の形に変換しています。
(defmacro with-bindings (pairs &body body)
(let ((bindings (loop for (key val) on pairs by #'cddr
collect `(,(intern (string key)) ,val))))
`(let ,bindings
,@body)))Code language: Lisp (lisp)
loopのfor (key val) on pairs by #'cddrが、2個ずつ取り出す部分です。by #'cddrで1個ではなく2個ずつ進みます。
また、キーワード:xは、internで普通のシンボルxに変換します。
bindingsが用意できたら、これをマクロ展開のバッククォート内で使っています。
展開を確認します。
(macroexpand-1 '(with-bindings (:x 1 :y 2 :z 3) (+ x y z)))
;=> (LET ((X 1) (Y 2) (Z 3))
; (+ X Y Z))Code language: Lisp (lisp)
6.2. rotateマクロを作る
次は、rotate を作ります。
rotateマクロは、swapマクロの応用で、可変長の引数を入れ替えます。
(defparameter a 1)
(defparameter b 2)
(defparameter c 3)
(rotate a b c)
a ; => 3
b ; => 1
c ; => 2Code language: Lisp (lisp)
(rotate a b c)で「a←b、b←c、c←a」というローテートにします。
素朴に考えると、末尾の値をgensymで退避してから右から左へ順に代入し、最後に先頭へ退避した値を入れれば壊れません。
(defmacro rotate (&rest vars)
(let ((tmp (gensym "TMP")))
`(let ((,tmp ,(car (last vars))))
,@(loop for (curr prev) on (reverse vars) by #'cdr
when prev
collect `(setf ,prev ,curr))
(setf ,(car vars) ,tmp))))Code language: Lisp (lisp)
展開を確認します。
(macroexpand-1 '(rotate a b c))
;=> (LET ((#:TMP C))
; (SETF B C)
; (SETF A B)
; (SETF A #:TMP))Code language: Lisp (lisp)
with-bindingsはloopで2個ずつ取り出す、rotateはreverseしてペアを作る。
操作の中身は違っても、バッククォートの外でリストを処理してから展開する、という流れは同じです。
6.3. do-while を作る
ここまでの知識を組み合わせて、少し複雑なマクロを作ります。
C言語スタイルの do-while です。
(do-while
(setf line (read-line stream nil nil))
(process line)
:while line)Code language: Lisp (lisp)
while がループの先頭で条件を検査するのに対し、do-while はボディを1回実行してから条件を検査します。
:while キーワードで条件を区切り、ボディを先に書くスタイルにします。
これは、単純な置き換えではできません。
コード自体を操作することになります。
(defmacro do-while (&body body-and-condition)
(let ((pos (position :while body-and-condition)))
(unless pos
(error "do-while requires :while keyword"))
(let ((body (subseq body-and-condition 0 pos))
(condition (nth (1+ pos) body-and-condition)))
`(loop
(progn ,@body)
(unless ,condition (return))))))Code language: Lisp (lisp)
do-whileマクロの引数は、&bodyで渡され、body-and-conditionというひとつのリストに、ボディと条件式が混在した状態で渡ってきます。
これを、ボディと条件式に分ける必要があります。
まず、body-and-condition の中から、:whileを探して、pos に代入します。positionはリストの中から要素を探してインデックスを返します。
見つからなければnilを返すので、unlessでエラーにしています。
次に、インデックス0から :while (pos)より前の部分列を作って body にします。
反対に、:whileの次 (1+ pos)番目の要素を condition に入れます。
最後が、展開コードの組み立てです。bodyのリストは,@で展開してprognに並べ、conditionをunlessの条件にしています。
展開結果を確認します。
(macroexpand-1
'(do-while
(print "tick")
(incf n)
:while (< n 3)))
;=> (LOOP
; (PROGN (PRINT "tick") (INCF N))
; (UNLESS (< N 3) (RETURN)))Code language: Lisp (lisp)
動作を確認します。
(defparameter n 0)
(do-while
(print n)
(incf n)
:while (< n 3))
; 0
; 1
; 2Code language: Lisp (lisp)
n が最初から 3 以上でも、ボディが1回は実行されます。
(defparameter n 10)
(do-while
(print "executed once")
:while (< n 3))
; "executed once" ; 条件を満たさなくても1回は実行されるCode language: Lisp (lisp)
:while が見つからないときは error でわかりやすく失敗させています。
マクロの引数が不正なときは、展開時点でエラーを出す方が、実行時に謎の挙動を起こすより親切です10。
- 式のテキストを取得するこのパターンは、Common Lisp の
*macroexpand-hook*変数を使った展開フックや、SLIME のslime-macroexpand-1コマンドとも組み合わせられます。 – Common Lisp HyperSpec: macroexpand-hook - バッククォート構文はリーダーマクロとして実装されており、Common Lisp 標準で定義されています。入れ子のバッククォートは動作が複雑になるため、Paul Graham も On Lisp の中で「nested backquote is usually a tedious debugging exercise」と述べています。 – Common Lisp HyperSpec: Backquote
loop自体にもwhileキーワードを持っており、(loop while condition do ...)と書けます。自作のwhileマクロはその簡略表記として活用できます。 – Common Lisp HyperSpec: loop&bodyは CLHS では&restと同一の動作をするとされており、違いはあくまで意味論的なものです。Emacs のlisp-indent-functionプロパティによりインデントが制御されており、defmacroが&bodyを検出したとき自動的に適切なインデントルールが設定されます。 – Common Lisp HyperSpec: Macro Lambda Lists- SLIMEには
C-c M-m(slime-macroexpand-all)で全展開するコマンドもあります。展開結果はバッファに表示されるため、長い展開結果でも全体を確認できます。 – SLIME Manual: Macro expansion - Scheme では
syntax-rulesによる衛生的マクロ(hygienic macros)がデフォルトで、変数キャプチャが自動的に防がれます。Common Lisp のdefmacroは非衛生的ですが、gensymとパッケージシステムを組み合わせることで実用上の問題はほとんど回避できます。 – Hygienic macro – Wikipedia gensymが生成する番号は内部カウンタ*gensym-counter*で管理されています。数値引数でカウンタを指定する古い使い方は deprecated になっており、現代のコードでは文字列プレフィックスのみを渡すスタイルが推奨されています。 – Common Lisp HyperSpec: gensym- アナフォリック(anaphoric)という名前は言語学の照応(anaphora)に由来します。「it」「this」のような代名詞が直前の文脈を指すのと同じように、マクロの
itが直前の条件式の結果を指す、という比喩です。Paul Graham が On Lisp で命名・普及させました。 – On Lisp: Chapter 14 - 標準マクロの
incf・decf・push・rotatefなどは、引数の各サブフォームをちょうど一度だけ左から右の順で評価することが CLHS で保証されています。自作マクロでもこの規約に倣うと利用者が安心して使えます。 – Common Lisp HyperSpec: setf - Common Lisp には
loop-finishという標準マクロがあり、loopの中から明示的に終了できます。do-whileのような独自ループ構文でも、終了条件の設計は重要で、returnやreturn-fromとの組み合わせを検討する価値があります。 – Common Lisp HyperSpec: loop-finish