[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [表紙] | [目次] | [索引] | [検索] [上端 / 下端] |
マクロ(macros)により、 新たな制御構造の構文を定義したり、他の言語の機能を定義したりできます。 マクロは関数のように定義しますが、値の計算方法を指示するかわりに、 値を計算するための別のLisp式の計算方法を指示します。 この式をマクロの展開形(expansion)と呼びます。
マクロでこのようなことができるのは、 関数が評価済みの引数を操作するのに対して、 マクロは引数の未評価の式を操作するからです。 そのため、これらの引数の式やその一部を含む展開形を構築できるのです。
実行速度のために普通の関数でできることにマクロを使うのであれば、 そのかわりにインライン関数を使うことを考えてください。
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [表紙] | [目次] | [索引] | [検索] [上端 / 下端] |
C言語の演算子++
のように、 変数の値を増加させるLispの構文を定義したいとしましょう。 (inc x)
のように書いて、 (setq x (1+ x))
のような効果を得たいのです。 これを行うマクロ定義はつぎのようになります。
(defmacro inc (var) (list 'setq var (list '1+ var))) |
これを(inc x)
のように呼び出すと、 引数varはシンボルx
になります。 関数のようにx
の値ではありません。 マクロの本体では、これを使って展開形(setq x (1+ x))
を構築します。 マクロ定義がこの展開形を返すと、 Lispはそれを評価することに進み、x
を増やします。
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [表紙] | [目次] | [索引] | [検索] [上端 / 下端] |
マクロ呼び出しはマクロ名で始まるリストであり、 関数呼び出しとほとんど同じに見えます。 リストの残りの要素はマクロの引数です。
マクロ呼び出しの評価は、関数呼び出しの評価のように始められますが、 1つだけ重要な違いがあります。 マクロの引数は、マクロ呼び出しに現れた実際の引数です。 マクロ定義に渡すまえに、それらを評価しません。 一方、関数の引数は、関数呼び出しのリストの要素を評価した結果です。
引数を得ると、Lispは関数定義を起動するのと同様にマクロ定義を起動します。 マクロの引数変数は、マクロ呼び出しの引数値や &rest
引数の場合にはそれらのリストに束縛されます。 そうして、マクロ本体を実行し、関数本体と同様に値を返します。
マクロと関数の重要な違いの2つめは、 マクロ本体が返した値はマクロ呼び出しの値ではないことです。 戻り値は値を計算するためのかわりの式であり、 これをマクロの展開形(expansion)といいます。 Lispインタープリタは、マクロから戻ってくると、 ただちに展開形を評価することへ進みます。
展開形は、通常どおりに評価されるので、 展開形から他のマクロを呼び出してもかまいません。 同一のマクロを呼び出してもかまいませんが、 それは一般的ではありません。
macroexpand
を呼ぶと、指定したマクロの展開形を調べることができます。
macroexpand
が返す値である。 formが始めからマクロ呼び出しでなければ、 与えられたとおりのものを返す。
macroexpand
はformの部分式を調べないことに注意してほしい (ただし、マクロ定義によっては調べるかもしれない)。 部分式がマクロ呼び出しであったとしても、 macroexpand
はそれらを展開しない。
関数macroexpand
は、インライン関数の呼び出しは展開しない。 インライン関数の呼び出しを理解することは普通の関数呼び出しを理解するのと かわりないので、通常、そのような展開を行う必要はない。
environmentを指定すると、 それは、現在定義済みのマクロを隠すマクロ定義の連想リストを表す。 バイトコンパイルではこの機能を使う。
(defmacro inc (var)
(list 'setq var (list '1+ var)))
=> inc
(macroexpand '(inc r))
=> (setq r (1+ r))
(defmacro inc2 (var1 var2)
(list 'progn (list 'inc var1) (list 'inc var2)))
=> inc2
(macroexpand '(inc2 r s))
=> (progn (inc r) (inc s)) ; ここでは
|
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [表紙] | [目次] | [索引] | [検索] [上端 / 下端] |
なぜ、マクロの展開形をわざわざ計算してから展開形を評価するのか、 疑問に思うかもしれません。 なぜ、マクロ本体で望みの結果を直接出さないのでしょう? その理由には、コンパイルが関係しています。
コンパイルするLispプログラムにマクロ呼び出しが現れると、 Lispコンパイラは、インタープリタがするのと同様にマクロ定義を呼び出し、 その展開形を受け取ります。 この展開形を評価するかわりに、コンパイラは、 展開形がプログラムに直接現れたかのようにそれをコンパイルします。 その結果、コンパイル済みのコードは、マクロが意図した値と副作用を生じ、 かつ、実行速度はコンパイルした速度になるのです。 マクロ本体そのもので値と副作用を計算したのでは、 このように動作しません。 コンパイル時に計算してしまい、それでは意味がありません。
マクロ呼び出しが正しくコンパイルされるためには、 それらの呼び出しをコンパイルするときに、 Lisp内でマクロが定義済みである必要があります。 コンパイラには、読者がこのようにすることを補佐する機能があります。 コンパイル対象のファイルにフォームdefmacro
が含まれていると、 そのファイルの残りをコンパイルするあいだは、 一時的にマクロを定義します。 この機能が動作するためには、 defmacro
を同じファイルの最初に利用する箇所よりまえに 入れておく必要があります。
ファイルをバイトコンパイルすると、 そのファイルのトップレベルにあるrequire
の呼び出しを実行します。 これは、ファイルを正しくコンパイルするために必要なパッケージを表します。 コンパイル中に必要なマクロ定義が使えることを保証する1つの方法は、 それらのマクロを定義するファイルを require
に指定しておくことです(see 節 14.6 機能)。 コンパイル済みのプログラムを実行するときに、 マクロを定義したファイルをロードしてしまうことを避けるには、 require
の呼び出しの周りにeval-when-compile
を書いておきます (see 節 15.5 コンパイル時の評価)。
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [表紙] | [目次] | [索引] | [検索] [上端 / 下端] |
Lispのマクロは、そのCARがmacro
であるリストです。 そのCDRは関数であるべきです。 マクロの展開は、マクロ呼び出しの未評価の引数式に (apply
で)関数を適用して動作します。
無名関数のように無名Lispマクロを使うことも可能ですが、 けっしてしないでしょう。 mapcar
のようなファンクショナルに無名マクロを渡す意味がないからです。 実用上は、すべてのLispマクロには名前があり、 普通、スペシャルフォームdefmacro
で定義します。
defmacro
は、シンボルnameをつぎのようなマクロとして定義する。
(macro lambda argument-list . body-forms) |
(このリストのCDRは関数、つまり、ラムダ式であることに注意。) このマクロオブジェクトは、nameの関数セルに格納される。 フォームdefmacro
を評価した結果、返される値はnameであるが、 通常この値は無視する。
argument-listの形式と意味は、関数のそれと同じであり、 キーワード&rest
や&optional
を使ってもよい (see 節 11.2.3 引数リストのその他の機能)。 マクロにも説明文字列を指定できるが、 マクロを対話的に呼び出すことはできないので、 interactive
宣言は無視する。
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [表紙] | [目次] | [索引] | [検索] [上端 / 下端] |
マクロでは、定数部分と非定数部分を組み合わせた大きなリスト構造を 構築する必要がしばしばあります。 これを簡単に行うためには、 (通常、バッククォート(backquote)と呼ばれる)``'構文を 使います。
バッククォートにより、リストの要素を選択に評価しつつ、 リストをクォートできます。 もっとも単純な場合、これはスペシャルフォームquote
(see 節 8.3 クォート)と 等価です。 たとえば、つぎの2つのフォームは等価な結果になります。
`(a list of (+ 2 3) elements) => (a list of (+ 2 3) elements) '(a list of (+ 2 3) elements) => (a list of (+ 2 3) elements) |
バッククォートの引数の内側にある特別な印`,'は、 値が定数ではないことを表します。 バッククォートは、リスト構造の中の`,'の引数を評価し、 値で置き換えます。
(list 'a 'list 'of (+ 2 3) 'elements) => (a list of 5 elements) `(a list of ,(+ 2 3) elements) => (a list of 5 elements) |
`,'による置き換えは、リスト構造の深いレベルでも許されます。 たとえば、つぎのとおりです。
(defmacro t-becomes-nil (variable) `(if (eq ,variable t) (setq ,variable nil))) (t-becomes-nil foo) == (if (eq foo t) (setq foo nil)) |
特別な印`,@'を使って、 評価結果を結果となるリストに繋ぎ合わせる(splice)こともできます。 繋ぎ合わせたリストの要素は、結果となるリストの他の要素と同じレベルになります。 ``'を使わない等価なコードはしばしば読み難くなります。 例をあげましょう。
(setq some-list '(2 3)) => (2 3) (cons 1 (append some-list '(4) some-list)) => (1 2 3 4 2 3) `(1 ,@some-list 4 ,@some-list) => (1 2 3 4 2 3) (setq list '(hack foo bar)) => (hack foo bar) (cons 'use (cons 'the (cons 'words (append (cdr list) '(as elements))))) => (use the words foo bar as elements) `(use the words ,@(cdr list) as elements) => (use the words foo bar as elements) |
19.29版よりまえのEmacsの旧版では、 ``'の構文は異なっていて、 バッククォート構文全体を囲む括弧の余分なレベルが必要でした。 同様に、`,'や`,@'の置換でも、 `,'や`,@'、および後続の式を囲む括弧の余分なレベルが1つ必要でした。 古い構文では、``'、 `,'、`,@'と後続の式とのあいだには 空白が必要でした。
この構文も受け付けますが、これはEmacsの旧版との互換性のためであり、 新しいプログラムでは使わないことを勧めます。
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [表紙] | [目次] | [索引] | [検索] [上端 / 下端] |
マクロ展開に関する基本的事実には、直観的でない結果があります。 本節では、問題を引き起こしかねない重要な結果を説明し、 問題を回避するための規則を説明します。
12.6.1 マクロ引数の複数回評価 The expansion should evaluate each macro arg once. 12.6.2 マクロ展開形内のローカル変数 Local variable bindings in the expansion require special care. 12.6.3 展開形におけるマクロ引数の評価 Don't evaluate them; put them in the expansion. 12.6.4 マクロは何回展開されるか Avoid depending on how many times expansion is done.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [表紙] | [目次] | [索引] | [検索] [上端 / 下端] |
マクロを定義するときには、展開形を実行するときに、 引数が何回評価かされるかに注意を払う必要があります。 つぎの(繰り返しを行う)マクロで、この問題を示しましょう。 このマクロで、Pascalにあるような単純な『for』ループを書けます。
(defmacro for (var from init to final do &rest body) "Execute a simple \"for\" loop. For example, (for i from 1 to 10 do (print i))." (list 'let (list (list var init)) (cons 'while (cons (list '<= var final) (append body (list (list 'inc var))))))) => for (for i from 1 to 3 do (setq square (* i i)) (princ (format "\n%d %d" i square))) ==> (let ((i 1)) (while (<= i 3) (setq square (* i i)) (princ (format "%d %d" i square)) (inc i))) -|1 1 -|2 4 -|3 9 => nil |
このマクロの引数、from
、to
、do
は、 『シンタックスシュガー』であり、完全に無視します。 (from
、to
、do
などの)余分な単語を マクロ呼び出しのこの引数位置に書けるようにするのです。
バッククォートを使って単純化した等価な定義をつぎに示します。
(defmacro for (var from init to final do &rest body) "Execute a simple \"for\" loop. For example, (for i from 1 to 10 do (print i))." `(let ((,var ,init)) (while (<= ,var ,final) ,@body (inc ,var)))) |
この定義の(バッククォートありとなしの)どちらの形式でも、 各繰り返しごとにfinalが評価されるという欠陥があります。 finalが定数ならば、これは問題になりません。 たとえば(long-complex-calculation x)
のような、 より複雑なフォームであると、実行速度をかなり遅くしてしまいます。 finalに副作用があると、複数回評価するのは正しくありません。
繰り返し評価することがマクロの意図している目的の一部でなければ、 よく設計されたマクロ定義では、 引数をちょうど1回だけ評価するような展開形を生成して、 上のような問題を回避するように手立てします。
(let ((i 1) (max 3)) (while (<= i max) (setq square (* i i)) (princ (format "%d %d" i square)) (inc i))) |
このような展開形を作るマクロ定義はつぎのようになります。
(defmacro for (var from init to final do &rest body) "Execute a simple for loop: (for i from 1 to 10 do (print i))." `(let ((,var ,init) (max ,final)) (while (<= ,var max) ,@body (inc ,var)))) |
残念なことに、この修正は、 次節に説明する別の問題を引き起こします。
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [表紙] | [目次] | [索引] | [検索] [上端 / 下端] |
前節では、for
の定義をつぎのように修正して、 マクロ引数を適切な回数だけ評価する展開形にしました。
(defmacro for (var from init to final do &rest body) "Execute a simple for loop: (for i from 1 to 10 do (print i))." `(let ((,var ,init) (max ,final)) (while (<= ,var max) ,@body (inc ,var)))) |
for
の新しい定義には、新たな問題があります。 ユーザーが予期していないローカル変数max
を導入しているのです。 これは、つぎのような場合、問題を引き起こします。
(let ((max 0)) (for x from 0 to 10 do (let ((this (frob x))) (if (< max this) (setq max this))))) |
for
の本体内でのmax
の参照は、 ユーザーが束縛したmax
を参照するものと期待されていますが、 実際にはfor
が作った束縛を使います。
これを修正するには、max
のかわりに、 インターンしてないシンボル(see 節 7.3 シンボルの作成とインターン)を使います。 インターンしてないシンボルは、他のシンボルと同様に、 束縛したり参照したりできますが、for
で作ったので、 ユーザープログラムには現れていないことがわかっています。 インターンしてないので、ユーザーがプログラムのあとの部分で 参照する方法もありません。 for
で使った箇所以外には現れえないのです。 このように動作するfor
の定義をつぎに示します。
(defmacro for (var from init to final do &rest body) "Execute a simple for loop: (for i from 1 to 10 do (print i))." (let ((tempvar (make-symbol "max"))) `(let ((,var ,init) (,tempvar ,final)) (while (<= ,var ,tempvar) ,@body (inc ,var))))) |
これは、max
という名前のインターンしてないシンボルを作成し、 もとの式に現れていたインターンしたシンボルmax
のかわりに 展開形内部で使います。
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [表紙] | [目次] | [索引] | [検索] [上端 / 下端] |
eval
(see 節 8.4 評価(eval))を呼び出すなどして、 マクロ定義そのものの中でマクロ引数の式を評価すると、 別の問題を生じます。 引数でユーザーの変数を参照する場合、 ユーザーがマクロ引数の1つと同じ名前を使っていると、 問題になります。 マクロ本体の内側では、マクロ引数の束縛が最ローカルな束縛ですから、 そのフォームの内側からの参照は、この束縛を使います。 例を示しましょう。
(defmacro foo (a) (list 'setq (eval a) t)) => foo (setq x 'b) (foo x) ==> (setq b t) => t ; |
ユーザーの引数の名前がa
かx
かで違いがでます。 というのは、マクロ引数の変数a
とa
が衝突するからです。
マクロ定義内でeval
を呼び出したときの別の問題点は、 コンパイルしたプログラムでは、意図した動作をしないだろうということです。 バイトコンパイラは、プログラムをコンパイル中にマクロ定義を実行しますから、 (eval
で参照したい)プログラムそのものの計算は行われず、 そのローカル変数の束縛も存在しません。
これらの問題を回避するには、 マクロ展開の計算過程では、引数の式を評価しないことです。 そのかわりに、マクロ展開では式の置換を使って、 展開時にその値が計算されるようにします。 このようにすれば、本章の他の例題は動作します。
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [表紙] | [目次] | [索引] | [検索] [上端 / 下端] |
関数を解釈実行しているときには、マクロ呼び出しを評価するたびに展開しますが、 コンパイルした関数では、(コンパイル時に)1回だけ展開します。 この違いが問題になることもあります。 マクロ定義に副作用があると、 マクロを何回展開したかに依存して動作が異なります。
したがって、マクロ展開の計算では、 本当になにをしているのか理解していない限り、副作用は避けてください。
特別な種類の副作用の1つ、つまり、 Lispオブジェクトを構築することは回避できません。 ほとんどすべてのマクロ展開では、リストを構築し、 それがマクロの重要な点でもあります。 これは、通常、安全ですが、 1つだけ注意する必要があります。 読者が構築したオブジェクトが、 マクロ展開形の中のクォートした定数の一部であるときです。
コンパイル時にマクロを1回だけ展開すると、 コンパイル中にはオブジェクトは一度だけ作られます。 しかし、解釈実行中には、マクロ呼び出しを行うたびにマクロを展開するので、 そのたびに新たなオブジェクトが作成されたことを意味します。
見通しのよいほとんどのLispコードでは、この違いは関係ありません。 マクロ定義で構築したオブジェクトに副作用のある操作を行うと 違いが出てきます。 したがって、問題を回避するには、 マクロ定義で構築したオブジェクトに副作用のある操作は行わない ということです。 そのような副作用がどのように問題を引き起こすのか、例をあげましょう。
(defmacro empty-object () (list 'quote (cons nil nil))) (defun initialize (condition) (let ((object (empty-object))) (if condition (setcar object condition)) object)) |
initialize
を解釈実行しているときには、 initialize
を呼び出すたびに新たなリスト(nil)
が作られます。 したがって、2つの呼び出しのあいだで副作用が残ることはありません。 initialize
をコンパイルしてあると、 マクロempty-object
はコンパイル時に展開され、 1つの『定数』(nil)
を作りますが、 これは、initialize
を呼び出すたびに、 再利用され変更されてしまいます。
このような病的な場面を回避する1つの方法は、 empty-object
を、メモリ割り付けではなく、 ある種の定数と考えることです。 '(nil)
のような定数にsetcar
は使わないでしょうから、 (empty-object)
も自然にそのように使わないでしょう。
[ << ] | [ >> ] | [表紙] | [目次] | [索引] | [検索] [上端 / 下端] |