マクロを使うと、自分用のスペシャルフォームをつくることができます。 マクロは変換子手続きに結びついたシンボルのことです。Scheme はマクロ式 (つまり、先頭がマクロであるフォーム)にであうと、マクロ変換子をマクロ式の サブフォームに適用し変形の結果を評価します。
概念としては、マクロは、あるコードのテキストを別のコードのテキスト に変形する純粋にテキストの変形を指定するものです。 この種の変形は、複雑で頻繁にあらわれるテキスト上のパターンを 略記するのに便利です。
マクロはスペシャルフォーム define-macro を使って定義します
(が、A.3節も参照してください)。1
たとえば、おつかいになっている Scheme に条件のスペシャルフォーム
when がない場合には、when を以下のようにマクロで定義することが
できます。
(define-macro when (lambda (test . branch) (list 'if test (cons 'begin branch))))
これは、when式を同等のif式に変換するwhen変換子を定義して
います。ここで定義したこのマクロを使えば、when式
(when (< (pressure tube) 60) (open-valve tube) (attach floor-pump tube) (depress floor-pump 5) (detach floor-pump tube) (close-valve tube))
は別の式に変換されます。when変換子をwhen式のサブフォームに
適用した結果は以下のようになります。
(apply (lambda (test . branch) (list 'if test (cons 'begin branch))) '((< (pressure tube) 60) (open-valve tube) (attach floor-pump tube) (depress floor-pump 5) (detach floor-pump tube) (close-valve tube)))
変形は以下のようになります。
(if (< (pressure tube) 60) (begin (open-valve tube) (attach floor-pump tube) (depress floor-pump 5) (detach floor-pump tube) (close-valve tube)))
こうしてから、Scheme はこの式をほかのものと同じように評価します。
別の例、when の対の unless マクロ定義はこんな風になります。
(define-macro unless (lambda (test . branch) (list 'if (list 'not test) (cons 'begin branch))))
もうひとつの方法としては、when を unless の定義内で呼ぶことがで
きます。
(define-macro unless (lambda (test . branch) (cons 'when (cons (list 'not test) branch))))
マクロ展開は別のマクロを参照することができます。
マクロ変換子はいくつかのS式をとり、フォームとして使える
ひとつのS式を作りだします。典型的な出力はリストです。
whenの例では、出力のリストは
(list 'if test (cons 'begin branch))
を使ってつくります。ここで、testはマクロの最初のサブフォーム
に束縛されています。つまり、
(< (pressure tube) 60)
です。また、branch はマクロののこりのサブフォームに束縛
されています。
((open-valve tube) (attach floor-pump tube) (depress floor-pump 5) (detach floor-pump tube) (close-valve tube))
出力されたリストはずいぶん複雑なものです。whenより
もっと野心的なマクロでは、出力リストをつくるのに、非常に
凝った構築プロセスを踏まなければならにことは、容易に想像が
つきます。このような場合には、マクロの出力フォームを
テンプレートとして指定できると便利です。マクロの
それぞれの用途ごとに、マクロの引数を適切な場所に挿入して
テンプレートを埋めます。したがって、
(list 'IF test (cons 'BEGIN branch))
は次のものをより便利に書いたものです。
`(IF ,test (BEGIN ,@branch))
when のマクロ定義を次のように改造することができます。
(define-macro when (lambda (test . branch) `(IF ,test (BEGIN ,@branch))))
テンプレートのフォーマットは前述のリスト構築とは
ちがって、出力リストの形を直接的に見えるように表示します。
バッククウォート(`)はリストのテンプレートを導入します。
テンプレートの要素は、結果のリストに、そのとおりにあらわれ
ます。例外は、接頭辞として、コンマ(‘,’)あるいは
コンマスプライス(‘,@’)がついている場合です。
(説明のために、テンプレートの中で、そのとおりにあらわれる要素を
大文字で書いてあります。)
コンマとコンマスプライスはマクロ引数をテンプレートのなかに 入れるために使います。コンマは、続く式の評価結果を挿入します。 コンマスプライスは続く式の評価結果を継ぎあわせたあとに挿入します。 すなわち、一番そとがわの括弧をとりのぞきます。(ということは、 コンマスプライスによって導入される式はリストでなければならない ということです。)
例では、test と branch に束縛された値を与えると、
テンプレートが次のように、要求どおり展開されていることが分ります。
(IF (< (pressure tube) 60) (BEGIN (open-valve tube) (attach floor-pump tube) (depress floor-pump 5) (detach floor-pump tube) (close-valve tube)))
二つの引数の選言フォームmy-orは次のように定義できそうである。
(define-macro my-or (lambda (x y) `(if ,x ,x ,y)))
my-orはふたつの引数をとり、最初に真(非#f)になった値を
返します。特に、二番目の引数は最初の引数が偽になったときにのみ
評価されます。
(my-or 1 2) => 1 (my-or #f 2) => 2
ここに書いたmy-orマクロには問題がひとつあります。
最初の引数は、それが真の場合、2度評価されます。
一度目はifテストでそして、もう一度は、「then」の枝でです。
これは、最初の引数が副作用をもつ場合、望ましくない振舞いを
発生します。たとえば、
(my-or (begin (display "doing first argument") (newline) #t) 2)
は"doing first argument"を二度表示します。
これは、ifテストの結果をローカル変数に保存することで回避できます。
(define-macro my-or (lambda (x y) `(let ((temp ,x)) (if temp temp ,y))))
これでほとんど OK なのですが、二番目の引数がたまたま
マクロ定義で使っているのと同じtempという識別子を含んでいると
こまったことになります。たとえば、
(define temp 3) (my-or #f temp) => #f
ほんとは 3 になるべきなのに。このチョンボは最初の引数の値
(#f)を保存するのにローカル変数tempを使ったために、
第二引数の変数tempが、このマクロで導入したtempに
よって捕捉されてしまったからです。
これを回避するには、マクロ定義中でつかうローカル変数を 注意深く選択する必要があります。こうした変数に突飛ななまえ を選択し、だれもこの名前を使わないように強くねがうという こともありかもしれません。
(define-macro my-or (lambda (x y) `(let ((+temp ,x)) (if +temp +temp ,y))))
これは+tempをマクロの外側のコードでは使わないという
暗黙の合意がなりたっていればうまく動くでしょう。
もちろん、こんな合意はいつかは破綻します。
冗長でよければ、もっと、あてになるのは、
他の手段で得られないことが保証されている作られたシンボル
を使うことです。手続きgensymは呼ばれるたびにユニークな
シンボルを生成する手続きです。これが、gensymをつかった
安全なmy-orの定義です。
(define-macro my-or (lambda (x y) (let ((temp (gensym))) `(let ((,temp ,x)) (if ,temp ,temp ,y)))))
この文書中で定義されたマクロの中には、簡潔にするために、gensym
を使う方法はとりません。変数捕捉の点については、既にみたように、
あまり、とりちらからない+接頭辞を使う方法を利用することにします。
このような+接頭辞識別子を上で概要を示した方法で gensym におきかえ
るのを忘れないようにするのは賢明な読者のかたにおまかせすることにします。
fluid-let
もうすこし、複雑なマクロfluid-let ( 5.2節)
の定義をみましょう。
(fluid-let ((x 9) (y (+ y 1))) (+ x y))
これを次のように展開したいとしましょう。
(let ((OLD-X x) (OLD-Y y)) (set! x 9) (set! y (+ y 1)) (let ((RESULT (begin (+ x y)))) (set! x OLD-X) (set! y OLD-Y) RESULT))
ここで、識別子OLD-X、OLD-Y、およびRESULTを
fluid-letフォームの式の中で捕捉されないようなシンボルに
したいわけです。
ここで、欲しいもの実現するfluid-letマクロを構築するのに専念します。
(define-macro fluid-let (lambda (xexe . body) (let ((xx (map car xexe)) (ee (map cadr xexe)) (old-xx (map (lambda (ig) (gensym)) xexe)) (result (gensym))) `(let ,(map (lambda (old-x x) `(,old-x ,x)) old-xx xx) ,@(map (lambda (x e) `(set! ,x ,e)) xx ee) (let ((,result (begin ,@body))) ,@(map (lambda (x old-x) `(set! ,x ,old-x)) xx old-xx) ,result)))))
このマクロの引数 xexe は fluid-let により導入される
変数/式の対のリスト、qbody は fluid-let の本体の式のリストです。
この例では、((x 9) (y (+ y 1))) と ((+ x y)) がそれぞれ、
xexe と body になります。
マクロ本体はローカル変数の束を導入します。xx は変数/式の対から
取り出した、変数のリストです。ee は対応する式のリストです。
old-xx は新しい識別子のリストで、それぞれが、xx の変数
ひとつひとつに対応しています。これらは、xx の値として
はいってきた値を保存しておくのに使います。これで、fluid-let
の本体の評価が終了したあとに xx お元にもどすことができます。
resultは別の新しい識別子で、fluid-let本体の値を保存するのに
つかいます。この例では xx は (x y)、qee は (9 (+ y 1))
です。おつかいのシステムの gensym の実装によりますが、
old-xx は例えば、(GEN-63 GEN-64)というリスト、qresult は
GEN-65 になります。
この例では、このマクロによって生成された出力リストは、次のような ものでしょう。
(let ((GEN-63 x) (GEN-64 y)) (set! x 9) (set! y (+ y 1)) (let ((GEN-65 (begin (+ x y)))) (set! x GEN-63) (set! y GEN-64) GEN-65))
これは、求めていたものにマッチしています。
1 MzScheme では
define-macro はdefmacro ライブラリを通して提供されています。
(require (lib "defmacro.ss")) を使ってこのライブラリをロード
してください。