マクロ
コードの生成とマクロについて調査してみた.
evalは引数で渡されたリストを評価する.
> (eval '(+ 1 2 3)) 6 > (eval (list '+ 1 2 3)) 6動的に生成したリストをevalに渡せば,そのリストは評価される.この方法で問題となるのは,リストがevalによってコードと解釈され評価されるのが実行時だという点である.これはコストが高く,だいたいの場合,コンパイル時にリストをコードとしてしまう方が効率がよい.
マクロとは式の変換オペレータであり,コンパイル対象は変換後の式である.したがってevalのときのような問題を解決できる.マクロはdefmacroで定義する.
> (defmacro macro-hoge (x) (list '+ x 1)) MACRO-HOGE > (macro-hoge 1) 2マクロ定義を確認するには,macroexpand-1でマクロを展開して表示してみるのがよい.
> (macro-hoge 2) 3 > (macroexpand-1 '(macro-hoge 2)) (+ 2 1) ; T(macro-hoge 2)はマクロ展開されて(+ 2 1)となっている.多くの場合マクロは展開するとリストになるように作成する.だが,上のサンプルのように本体がlist式だと少々読みにくく,書きにくい.マクロを展開後の形に近い形式で記述するにはバッククオートを使用する.macro-hogeはバッククオートを使用して以下のように書ける.
> (defmacro macro-hoge (x) `(+ ,x 1)) MACRO-HOGE > (macro-hoge 2) 3 > (macroexpand-1 '(macro-hoge 2)) (+ 2 1) ; T実はバッククォートは単独で使用するだけなら,シングルクォートと変わらない.
> (defmacro macro-hoge (x) `(+ x 1)) MACRO-HOGE > (macroexpand-1 '(macro-hoge 2)) (+ X 1) ; Tこの例とその前の例との違いは,バッククォートの部分が
`(+ ,x 1)か
`(+ x 1)かという違いである.バッククォート内では,(コンマ)に続く式は評価される.
`(+ x 1)は単に(+ x 1)というリストになるが,
`(+ ,x 1)はxを評価した結果,マクロの引数が代入される.バッククォート内では,@(コンマアットマーク)も使用できる.,@は引数がリストの場合に,その要素を並べたものとなる.
> (defmacro macro-fuga (x) `(+ ,@x)) > (macroexpand-1 '(macro-fuga (1 2 3 4))) (+ 1 2 3 4) ; T
以下のようなマクロがあったとする.
> (defmacro macro-foo (x) `(let ((value 0)) ,@x (format t "value = ~A~%" value))) MACRO-FOO > (macro-foo (+ 1 1)) value = 0 NILこのマクロは引数にかかわらずvalueを0に設定し,出力するものである.だが,マクロへの引数をvalueに値を設定する式にすると予期せぬ結果となる.
> (macro-foo ((setf value 1))) value = 1 NIL > (macroexpand-1 '(macro-foo ((setf value 1)))) (LET ((VALUE 0)) (SETF VALUE 1) (FORMAT T "value = ~A~%" VALUE)) ; T何が起こっているかはmacroexpand-1の出力ではっきりする.マクロは式を変換するだけなので,このようなことがおこる(意図しない変数キャプチャ).もちろん意図してこのようなマクロを作成する場合もありうるが,多くの場合このような現象は避けたい.そのためにはマクロ内で使用する変数をインターンしないgensymというものを使用する.
> (defmacro macro-foo (x) (let ((value (gensym))) `(progn (setf ,value 0) ,@x (format t "value = ~A~%" ,value)))) MACRO-FOO > (macro-foo ((setf value 100))) value = 0 NIL > (macroexpand-1 '(macro-foo ((setf value 100)))) (PROGN (SETF #:G10566 0) (SETF VALUE 100) (FORMAT T "value = ~A~%" #:G10566)) ; Tgensymのシンボルはインターンされない.で,macroexpand-1の出力のように#:G10566(※)といったもので示されており,同出力から変数キャプチャは回避されている.マクロではgensymのシンボルを参照するためにバッククォートとコンマにつづくシンボル名を使用する.gensymは予期しない変数キャプチャだけでなく,マクロの引数の多重評価を回避するためにも使用される.
※ 今回たまたまこのような値になっただけで,処理系や実行したタイミングで異なる.