M.Hiroi's Home Page

Functional Programming

お気楽 Scheme プログラミング入門

[ PrevPage | Scheme | NextPage ]

マクロ

今までいろいろな関数を作ってきましたが、いずれも引数を評価するものでした。つまり、define で定義できる関数は引数を評価するタイプで、シンタックス形式のように引数を評価しない関数を定義することはできません。Scheme でプログラミングする場合、ほとんどの処理は define で定義する関数で作ることができますが、シンタックス形式のように引数を評価しない関数を定義した方が便利な場合もあります。このようなとき、役に立つのが「マクロ (macro)」です。

Scheme の場合、マクロは二種類あります。一つは仕様書 (R5RS, R7RS-small) で定義されているマクロで、これを「健全なマクロ」といいます。これに対し、昔から Lisp で使われているマクロを「伝統的なマクロ」といいます。多くの Scheme 処理系では、どちらのマクロも使えるようになっています。もちろん、Gauche にも伝統的なマクロが用意されています。まず最初に、伝統的なマクロについて説明します。

●C言語のマクロ

もし、あなたがC言語ユーザーであれば、マクロはお馴染みの機能ではないかと思います。ところが、Lisp / Scheme のマクロはC言語とはちょっと毛色が変わっているので、慣れ親しんだマクロだと思っていると、とんでもないことになります。まず最初に、C言語で使われているマクロについて簡単に説明しましょう。

たとえば、C言語ではファイルの終了を表すのに EOF という記号を使います。次の例を見てください。

リスト : C言語のマクロ (1)

while ((code = fgetc( stdin )) != EOF) {
  ... 処理 ...
}

fgetc() はファイルからデータを読み込むC言語の関数です。これはファイルが終了するまでデータを読み込む、という処理をC言語でプログラムしたものです。EOF は記号といっても、Lisp / Scheme のシンボルと同じではありません。C言語にはシンボルのような機能はないのです。

実はこの EOF はファイル終了を表す整数値 (-1) なのです。単に数値を書くだけでは、その数値が何を意味をしているのか、前後の関係を理解しないと判断できません。つまり、ファイル終了時に関数 fgetc() が -1 を返すことを覚えていないと、この処理内容を理解することはできません。私たちは数値よりも意味のある記号の方が覚えやすいですよね。プログラムの場合も、単なる数値よりも記号を使った方が処理内容を把握しやすいのです。

そこで、EOF のような記号定数を定義しておき、同じ記号が出てくるたびにそれを一定の文字列 (EOF であれば -1) に置き換える、というような機能が欲しくなります。これが「マクロ」です。C言語では次のように定義します。

リスト : C言語のマクロ (2)

#define  EOF   (-1)

#define はマクロを定義する命令です。そして、EOF を -1 に置き換えることを「マクロ展開」といいます。C言語では、この操作を「プリプロセッサ」というプログラムが担当します。プリプロセッサはC言語のソースファイルを読み込み、マクロ定義を取り除きマクロ展開した結果を新しいファイルに書き込みます。そして、このファイルをコンパイルするのです。コンパイルの前にプリプロセッサが動作するところが、C言語の特徴といえるでしょう。

このようにC言語のマクロは、記号を定義した文字列に置き換える、という機能なのです。これに対して Lisp のマクロは、まさに Lisp らしいといえる機能を持っています。C言語との対比でいえば、「S 式を置き換える」と表現することができます。

●伝統的なマクロ

それでは、伝統的な Lisp におけるマクロの使い方を説明しましょう。Lisp ではマクロを関数のように定義します。Gauche の場合、伝統的なマクロを定義するには define-macro を使います。

define-macro の構文は define と同じです。define-macro で定義されたマクロは、次のような特徴を持ちます。

この 2 番目の機能が Lisp におけるマクロの特徴です。これを図に示すと、次のようになります。

[S式] ─  評価  -> [新しいS式] ─ 評価 -> [マクロの返り値]
      (マクロ展開)


                   図 : マクロの動作

S 式を評価することで新しい S 式を組み立てます。この部分がマクロ展開に相当します。そして、その S 式を評価した値がマクロの返り値となります。S 式を組み立てるということは、自動的にプログラムを作ることと同じですね。これは、リストにプログラムとデータの 2 つの役割を持たせている Lisp だからこそ可能なことなのです。

まず、マクロと関数の違いを理解するために、数を 2 乗する処理をマクロと関数で作ってみましょう。関数は簡単ですね。

リスト : 数を 2 乗する関数

(define (square x) (* x x))

マクロは次のように定義します。

リスト : 数を 2 乗するマクロ

(define-macro (m-square x) (list '* x x))

マクロ名は m-square としました。それでは、引数に (+ 1 2) を与えて m-square を評価してみます。

(m-square (+ 1 2))

仮引数 x に (+ 1 2) がセット(評価されないことに注意)

マクロの本体 (list '* x x) を評価する

=> (* (+ 1 2) (+ 1 2)) (S式が組み立てられる)

=> 9                   (S式を評価した結果)


        図 : マクロの実行

関数であれば引数 (+ 1 2) が評価されて、その返り値である 3 が square に渡されますね。マクロの場合、引数は評価されないので、仮引数 x には S 式である (+ 1 2) がそのままセットされます。

次に、マクロ本体を評価します。マクロを使いこなすポイントですが、まず評価したい S 式を組み立てることを考えます。最初の評価で S 式を組み立て、それを評価することで目的の処理を実現するのがマクロなのです。

この場合、引数の 2 乗する (* x x) という S 式を作ればいいわけです。list は引数を要素とする新しいリストを返す関数でしたね。この場合、シンボル * と x の値である (+ 1 2) が要素となったリストが返されます。

これでマクロ展開が終了しました。マクロの仮引数は、マクロ展開されるときだけ有効です。マクロ展開されたS式を評価するときは、それらの値は破棄されます。あとは、この S 式を評価して 9 という値が結果となります。

次に示す関数を引数に与えて square と m-square を評価すると、関数とマクロの違いがよくわかると思います。

gosh> (define (foo x) (format #t "~A " x) x)
foo
gosh> (square (foo 2))
2 4
gosh> (m-square (foo 2))
2 2 4

関数 square は、引数が評価されるので 2 が 1 回だけ出力されます。ところが m-square では、引数は評価されずに渡されて S 式 (* (foo 2) (foo 2)) が組み立てられます。その後、この S 式が評価されるので 2 が 2 回出力されるのです。

●マクロとコンパイラの関係

ところで、昔の Lisp 処理系では、引数を評価するタイプを EXPR 型や SUBR 型、引数を評価しないタイプを NEXPR 型や FSUBR 型と呼び、ユーザーが NEXPR 型の関数を定義することができました。Scheme や Common Lisp の場合、ユーザーが定義できるのは関数とマクロだけです。シンタックス形式の関数を定義する場合はマクロを使うことになります。

マクロを実行する場合、必ずマクロ展開が行われるため、通常の関数よりも実行時間は遅くなります。だったら、NEXPR 型の関数を定義できるようにした方が実行速度の点で有利なはずです。ところが、Scheme や Common Lisp では必要最低限のシンタックス形式を定義し、よく使われる制御構造はマクロで定義されています。これではインタプリタでの動作が遅くなります。

では、なぜ実行速度が遅くなるのにマクロを使っているのでしょう。それは、Common Lisp や多くの Scheme 処理系がコンパイラの使用を前提としているからです。たとえば、Gauche はプログラムをバイトコードにコンパイルしてから実行します。また、Common Lisp では CLISP がプログラムをバイトコードに、SBCL はネイティブコードにコンパイルします。今の実用的な Scheme (Common Lisp) 処理系のほとんどは、プログラムをバイトコードもしくはネイティブコードにコンパイルすることができます。

プログラムでマクロを呼び出している場所は、コンパイル時にマクロ展開されるため、コンパイル済みのコードにはマクロ呼び出しがなくなってしまうのです。つまり、コンパイル済みのコードは、マクロを呼び出す処理とマクロ展開の処理がなくなることにより、確実にインタプリタよりも高速に実行することができるのです。逆にいえば、コンパイラを使わないとマクロを効果的に使うことはできません。ご注意くださいませ。

●スタックの操作

今度は、もう少し複雑な例を見てみましょう。スタックを操作する関数をマクロで定義してみます。最初にスタックについて簡単に説明します。スタックの例として、バネ付きのトレイを取り上げます。次の図を見てください。


                図 : スタックの動作例

初めはトレイが入っていない空の状態です。ここにトレイを上から入れると、重さによってバネを圧縮し、次のトレイを追加できるようになります。もうひとつトレイを乗せると、さらにバネを圧縮し次のトレイを追加できるようになります。バネが限界まで圧縮されると、トレイは追加できません。トレイを取り出す場合は、上にあるトレイから取り出していきます。ひとつ取り出すと、その分バネが伸びて下にあるトレイが上に出てくるので、次のトレイを取り出すことができます。

このトレイをデータと考えてください。データ A をスタックに追加し (2)、次にデータ B を追加します (3)。データを取り出す場合、後から入れたデータ B が先に取り出され (4)、その次にデータ A が取り出されて、スタックが空になります (5)。スタックにデータを追加する操作をプッシュ (PUSH) といい、スタックからデータを取り出す操作をポップ (POP) といいます。このように、スタックは後から入れたデータが先に取り出されるので、「後入れ先出し (Last-In First-Out : LIFO)」と呼ばれます。

スタックはリストを使うと簡単に実現することができます。たとえば、大域変数 *stack* にスタックを保持することにします。プッシュはリストの先頭にデータを追加していくことで実現できます。これは cons を使えば簡単ですね。データをプッシュする push-stack は、次のようになります。

リスト : データの追加

(define *stack* '())

(define (push-stack x)
  (set! *stack* (cons x *stack*)))

それでは、実際に試してみましょう。

gosh> (push-stack 10)
(10)
gosh> *stack*
(10)
gosh> (push-stack 100)
(100 10)
gosh> *stack*
(100 10)

最初スタックにはデータがありませんから、*stack* は空で初期化しておきます。push-stack を実行するたびに、スタック *stack* にデータが追加されていきます。

次は、データをポップする pop-stack を作ります。ポップはリストの先頭にあるデータを取り出す操作です。データを取り出すには car を使えばいいですね。取り出したデータは *stack* から削除します。これには cdr を使えばいいでしょう。これを素直にプログラムすると、次のようになります。

リスト : データの取り出し

(define (pop-stack)
  (let ((x (car *stack*)))
    (set! *stack* (cdr *stack*))
    x))

let で局所変数 x を定義し、そこに *stack* の先頭要素をセットします。そして、*stack* の値を書き換えて、x の値を返します。

それでは、実際に試してみましょう。

gosh> (pop-stack)
100
gosh> *stack*
(10)
gosh> (pop-stack)
10
gosh> *stack*
()

確かにスタック *stack* からデータが削除され、そのデータが関数の返り値になっています。

●スタックを操作するマクロ

次はマクロを使って定義しましょう。関数 push-stack と pop-stack は、大域変数 *stack* にスタックを保持しましたが、これから作成するマクロは、スタック用の変数を引数として渡すことにします。Gauche にはマクロ push! と pop! が用意されているので、マクロ名は my-push! と my-pop! にします。my-push! は次のようになります。

リスト : データの追加 (マクロ版)

(define-macro (my-push! place x)
  (list 'set! place (list 'cons x place)))

それでは、実際に試してみましょう。

gosh> (define a '())
a
gosh> (my-push! a 10)
(10)
gosh> a
(10)
gosh> (my-push! a 20)
(20 10)
gosh> a
(20 10)

最初に変数 a を空リストに初期化しておきます。my-push! は list を使って S 式を組み立てます。place には a が、x には 10 がセットされているので、(set! a (cons 10 a)) という S 式が組み立てられます。この S 式が再度評価されて、変数 a にリスト (10) がセットされます。

これで引数 x も評価されることに注意してください。たとえば、x に (+ 1 2) を渡したとしましょう。マクロですから引数 x は評価されませんが、(list 'cons x place) のところで S 式 (cons (+ 1 2) a) が組み立てられ、マクロはその S 式を再度評価するので (+ 1 2) の結果 3 がスタックに格納されます。

my-pop! も同様に実現できます。

リスト : データの取り出し (マクロ版)

(define-macro (my-pop! place)
  (list 'let (list (list 'x (list 'car place)))
    (list 'set! place (list 'cdr place))
    x))

list を多用しているため複雑になってしまいましたが、これで let の構文を組み立てることができます。もっと簡単な定義方法もあるので心配しないでください。

それでは、実際に試してみましょう。

gosh> a
(30 20 10)
gosh> (my-pop! a)
30
gosh> a
(20 10)
gosh> (my-pop a)
20
gosh> a
(10)

正常に動作していますね。

●準クオート

ところで、マクロを定義するとき、S 式を組み立てるため list をたくさん使うことになり少々面倒です。実は、「準クォート (quasiquote)」という機能を使うと、S 式を簡単に組み立てることができます。なお、準クォートは R7RS-samll でも使用することができます。

準クォート ( ` ) はクォート ( ' ) と同様に引数の評価を行いません。'x が (quote x) と読み込まれるように、`x は (quasiquote x) として読み込まれます。ですが、準クォートの中でコンマ ( , ) で始まる S 式があると、その S 式を評価した値で置き換えられます。

簡単な例を示しましょう。

gosh> (define var 'pen)
var
gosh> var
pen
gosh> `(this is a ,var)
(this is a pen)

変数 var にはシンボル pen がセットされています。次の S 式の中で ,var は var を評価した値、つまり pen に置き換わるのです。なお、,x は (unquote x) として読み込まれます。

S 式の評価結果がリストの場合は、コンマアットマーク (,@) を使うことができます。,@ は (unquote-splicing x) として読み込まれます。,@ を使うと、リストをはずした値と置き換わります。,@ を使う場合、値がリストでなければエラーになります。次の例を見てください。

gosh> (define var '(pen))
var
gosh> var
(pen)
gosh> `(this is a ,var)
(this is a (pen))
gosh> `(this is a ,@var)
(this is a pen)

今度は変数 var にリスト (pen) がセットされました。次の S 式の中で ,var は (pen) に置き換わります。そして、その次の S 式の中で ,@var は pen に置き換わるのです。それから、コンマやコンマアットマークは準クォートの中でしか使うことができません。ほかの S 式の中で評価した場合はエラーとなります。ご注意ください。

それでは準クォートを使って my-push! と my-pop! を書き直してみましょう。

リスト : my-push! と my-pop! の改良

;;; データの追加
(define-macro (my-push1! place x)
  `(set! ,place (cons ,x ,place)))

;;; データの取り出し
(define-macro (my-pop1! place)
  `(let ((x (car ,place)))
     (set! ,place (cdr ,place))
     x))

my-push1! は x にコンマ ( , ) がついているので、x を評価した結果がスタックに積まれることに注意してください。準クォートを使った方が、どんな S 式が組み立てられて評価されるのかよくわかると思います。ですが、関数に比べるとマクロは理解するのが難しいと思います。そのマクロがどんなことをするのか、きちんとコメントを書いておいた方がよいでしょう。

●伝統的なマクロの問題点 (1)

ところで、(my-push! a x) をマクロ展開すると (set! a (cons x a)) になります。展開後の S 式に x が含まれていますね。この x は、どのように評価されるのでしょうか。次の例を見てください。

マクロ展開された S 式は、そのマクロを置き換えた状態で評価されます。上の例では、名前付き let の中で my-push! が評価されますが、この部分をマクロ展開後の S 式に置き換えて評価するのです。したがって、変数 x は名前付き let で定義された局所変数として扱われます。

関数呼び出しでは、関数の仮引数やその中で定義された変数を局所変数として扱いますが、それ以外の変数は大域変数として扱われます。ところがマクロの場合、マクロ展開時には関数呼び出しと同じ規則が適用されますが、展開後の S 式を評価するときは、マクロ呼び出し時に定義されている局所変数が有効になるのです。

それでは、次の例はどうなるのでしょうか。

大域変数 x にリストをセットし、my-pop! で取り出します。my-pop! をマクロ展開すると、my-pop! で定義している局所変数 x が大域変数 x を隠蔽するため、このマクロは正しく動作しません。このように、伝統的なマクロはマクロ展開した後で変数名が衝突することがあるのです。これが伝統的なマクロの欠点で、「変数捕捉 (variable capture)」といいます。

この場合、変数名が衝突しないように新しいシンボルを作成して局所変数として使います。関数 gensym は既存のシンボルと衝突しない新しいシンボルを作成して返します。

一般に、Lisp / Scheme はシンボルを管理するための「表」を持っています。大昔の Lisp 処理系では、システム内のシンボルを oblist というリストで管理していました。今では、ハッシュ表を使って管理するのが一般的です。ここでは「シンボル表」と呼ぶことにしましょう。普通のシンボルは、このシンボル表に登録されています。gensym はシンボル表に存在しないシンボルを新しく作成するので、既存のシンボルと衝突することはありません。

簡単な例を示しましょう。

gosh> (gensym)
G1
gosh> (gensym "ABC")
ABC2
gosh> (define a (gensym))
a
gosh> a
G3
gosh> (eq? a 'G3)
#f
gosh> (eq? a a)
#t

Gauche の場合、引数なしで gensym を呼び出すと、生成されるシンボル名は "G + 数字" になります。gensym に文字列を指定すると、シンボル名は "文字列 + 数字" になります。gensym で生成されるシンボルは Scheme システムの中で唯一のシンボルなので、同名のシンボルと eq? で比較しても #f が返されます。自分自身を eq? で比較すると、当然ですが #t になります。

このように、シンボル表に登録されていないシンボルは、それ以前の S 式で使われているシンボルと異なるわけですから、let の局所変数をこのシンボルで置き換えれば、他の変数と衝突することはなくなります。

gensym を使うと、my-pop! は次のようになります。

リスト : my-pop! の改良

(define-macro (my-pop2! place)
    (let ((x (gensym)))
        `(let ((,x (car ,place)))
            (set! ,place (cdr ,place))
	    ,x)))

まず最初に、let で局所変数 x を用意し、ここに gensym で生成したシンボルをセットします。次に、この x を使ってマクロ展開する S 式を組み立てます。これは今までのマクロ定義において、x の前にカンマ ( , ) を付けて x を評価するようにします。これで、マクロで使用する局所変数が、他の変数と衝突することを防ぐことができます。

gensym を使った my-pop! のマクロ展開は次のようになります。

define-macro でマクロ展開されるのは最後の S 式だけなので、gensym でシンボルを生成する処理はマクロ展開されませんが、生成されたシンボル G1 を使って S 式が組み立てられるわけです。これで変数捕捉を回避することができます。

●伝統的なマクロの問題点 (2)

伝統的なマクロを使う場合、もう一つ問題点があります。次のリストを見てください。

リスト : マクロの問題点 (2)

(define-macro (arithmetic-if test neg zero pos)
  (let ((var (gensym)))
    `(let ((,var ,test))
        (cond ((< ,var 0) ,neg)
	      ((= ,var 0) ,zero)
	      (else ,pos)))))

マクロ arithmetic-if は述語 test の返り値が負ならば引数 neg を、0 ならば引数 zero を、正ならば引数 pos を評価します。簡単な実行例を示します。

gosh> (arithmetic-if 0 (print -1) (print 0) (print 1))
0
#<undef>
gosh> (arithmetic-if -10 (print -1) (print 0) (print 1))
-1
#<undef>
gosh> (arithmetic-if 10 (print -1) (print 0) (print 1))
1
#<undef>

このように arithmetic-if は正常に動作していますが、次のように標準関数 < を書き換えると、arithmetic-if は正常に動作しなくなります。

gosh> (let ((< (lambda (x y) (> x y))))
        (arithmetic-if -10 (print -1) (print 0) (print 1)))
1
#<undef>

Scheme の標準関数を書き換えることはめったにないと思いますが、補助的な関数を作ってマクロから呼び出す場合は、その関数の定義を書き換えないように注意してください。

●マクロの再帰定義

マクロは再帰定義することもできます。簡単な例として and をマクロ定義してみましょう。次のリストを見てください。

リスト : and のマクロ定義

(define-macro (my-and . args)
  (case (length args)
    ((0) #t)
    ((1) (car args))
    (else `(if ,(car args) (my-and ,@(cdr args)) #f))))

名前は my-and としました。case はシンタックス形式で、cond と同様に条件分岐を行うときに使います。case は cond より奇妙な構文をもっています。

(case キーとなるS式
      ( キーリスト1 S式A1 S式A2 ... )
      ( キーリスト2 S式B1 S式B2 ... )
         ・・・・・
      ( キーリストM S式M1 S式M2 ... )
      ( else         S式T1 S式T2 ... ))


            図 : case の構文

case は最初にキーとなる S 式を受け取り、そのあと cond と同様に複数の節が続きます。cond には節の先頭に条件部がありましたが、case の場合はキーリストというものがあります。まず、キーとなる S 式を評価します。次に、この評価結果とキーリストに格納された要素を比較します。このとき、キーリスト本体や要素は評価されないことに注意してください。もし、等しいキーを見つけた場合は、その節の S 式を順番に実行します。

上図を見てください。case ではキーがキーリストの中に含まれているかチェックします。データの比較には述語 eqv? が適用されます。等しいキーを発見したら、その後ろの S 式を順番に実行していきます。

my-and は引数 args の長さを調べ、0 ならば #t がマクロ展開後の S 式となり、それを評価するので結果は #t になります。1 ならば、リスト args の先頭要素がマクロ展開後の S 式になり、それを評価します。それ以外の場合は args の先頭の要素を評価して、真ならば args の残りの要素を my-and に渡してマクロ展開します。偽ならば #f がマクロ展開後の S 式となり、その評価結果は #f になります。

たとえば、(my-and 1 2 3) は次のようにマクロ展開されます。

     (my-and 1 2 3)

            ↓

   (if 1 (my-and 2 3) #f)

            ↓

(if 1 (if 2 (my-and 3) #f) #f)

            ↓

   (if 1 (if 2 3 #f) #f)


図 : (my-and 1 2 3) のマクロ展開

簡単な実行例を示しましょう。

gosh> (my-and)
#t
gosh> (my-and 1)
1
gosh> (my-and 1 2)
2
gosh> (my-and 1 2 3)
3
gosh> (my-and 1 2 #f 3)
#f

正常に動作していますね。

●健全なマクロ

次は Scheme (R5RS, R7RS-small) の「健全なマクロ (hygienic macro)」について説明します。健全なマクロは syntax-rules という独自のパターン言語を使ってマクロを記述します。そして、define-sytax で大域的なマクロを定義し、let-sytax と letrec-syntax で局所的なマクロを定義します。sytax-rules の構文を下図に示します。

(syntax-rules (literal ...)
  (pattern_1 template_1)
  (pattern_2 template_2)

  ...

  (pattern_n template_n))


  図 : sytax-rules の構文

pattern は S 式の入力パターンを表します。S 式がパターンとマッチングする場合、それに対応するテンプレート template に変換します。パターンは、リスト、識別子 (literal)、定数、パターン変数、省略子 (...) などから構成されます。通常、パターンはリストの先頭にマクロ名を書き、そのあとにパターン変数などを記述します。なお、マクロ名はアンダーバー ( _ ) で代用することができます。

テンプレートはパターン変数、識別子、省略子などを使ってマクロ展開する S 式を記述します。入力された S 式とパターンがマッチングすると、パターン変数に対応する値が束縛され、その値を使って S 式が組み立てられます。このため、伝統的なマクロのように、バッククオートを使う必要はありません。

おおざっぱな説明ですが、あとは習うより慣れろということで、簡単なマクロを作っていきましょう。詳細な説明は Scheme の仕様書 (R7RS-small) をお読みくださいませ。

簡単な例として、数を 2 乗する処理をマクロで記述します。

リスト : 数を 2 乗するマクロ

(define-syntax m-square
  (syntax-rules ()
    ((_ x) (* x x))))

マクロ名は m-square で、x がパターン変数です。このパターンは S 式 (m-square s-exp) にマッチングします。たとえば、S 式 (m-square (+ 1 2)) は (_ x) とマッチングし、パターン変数 x の値は (+ 1 2) になります。テンプレートは (* x x) なので、(* (+ 1 2) (+ 1 2)) という S 式にマクロ展開されます。

それでは実行してみましょう。

gosh[r7rs.user]> (m-square (+ 1 2))
9
gosh[r7rs.user]> (m-square (begin (display "oops") (+ 1 2)))
oopsoops9

m-square は伝統的なマクロと同じく、S 式 (* (+ 1 2) (+ 1 2)) が組み立てられ、それを評価した結果が 9 になります。oops を表示させてみると、引数の S 式が 2 回評価されていることがわかります。

●伝統的なマクロとの違い (1)

次はスタックを操作するマクロ my-push! と my-pop! を作りましょう。次のリストを見てください。

リスト : スタックの操作

;;; データの追加
(define-syntax my-push!
  (syntax-rules ()
    ((_ place x) (set! place (cons x place)))))

;;; データの取得
(define-syntax my-pop!
  (syntax-rules ()
    ((_ place)
     (let ((x (car place)))
       (set! place (cdr place))
       x))))

my-push! と my-pop! は簡単です。たとえば、(my-push a 10) はパターン変数 place に a がセットされ、x に 10 がセットされます。テンプレートは (set! a (cons 10 a)) になり、この S 式が評価されてリストの先頭に 10 が追加されます。(my-pop! a) はパターン変数 place に a がセットされるので、テンプレートは次のように展開されます。

(let ((x (car a))
  (set! a (cdr a))
  x)

この S 式が評価されるので、my-pop! はリストの先頭要素を取り除き、その値を返します。

それでは実行してみましょう。

gosh[r7rs.user]> (define a '())
a
gosh[r7rs.user]> (my-push! a 10)
(10)
gosh[r7rs.user]> (my-push! a 20)
(20 10)
gosh[r7rs.user]> (my-pop! a)
20
gosh[r7rs.user]> (my-pop! a)
10
gosh[r7rs.user]> a
()

my-pop! では let で局所変数 x を使っていますが、Scheme の健全なマクロは変数名の衝突 (変数捕捉) を気にしなくても大丈夫です。次の例を見てください。

gosh[r7rs.user]> (define x '(1 2 3 4 5))
x
gosh[r7rs.user]> (let loop ((n 0))
 (cond ((< n 5) (display (my-pop! x)) (loop (+ n 1)))))
12345#<undef>

伝統的なマクロでは、let で局所変数 x を定義すると大域変数 x を隠蔽してしまいますが、健全なマクロは変数捕捉を回避してくれるので正常に動作します。これが健全なマクロの良いところです。

●伝統的なマクロとの違い (2)

もう一つ、伝統的なマクロとの違いを示しましょう。次のリストを見てください。

リスト : マクロ arithmetic-if

(define-syntax arithmetic-if
  (syntax-rules ()
    ((_ test neg zero pos)
     (let ((var test))
       (cond ((< var 0) neg)
             ((= var 0) zero)
             (else pos))))))

マクロ arithmetic-if は述語 test の返り値が負ならば引数 neg を、0 ならば引数 zero を、正ならば引数 pos を評価します。簡単な実行例を示します。

gosh[r7rs.user]> (arithmetic-if -10 (display -1) (display 0) (display 1))
-1#<undef>
gosh[r7rs.user]> (arithmetic-if 0 (display -1) (display 0) (display 1))
0#<undef>
gosh[r7rs.user]> (arithmetic-if 10 (display -1) (display 0) (display 1))
1#<undef>

伝統的なマクロは標準関数 < を書き換えると arithmetic-if は正常に動作しなくなりますが、健全なマクロならば大丈夫です。次の例を見てください。

gosh[r7rs.user]> (let ((< (lambda (x y) (> x y))))
(arithmetic-if 10 (display -1) (display 0) (display 1)))
1#<undef>
gosh[r7rs.user]>(let ((< (lambda (x y) (> x y))))
(arithmetic-if -10 (display -1) (display 0) (display 1)))
-1#<undef>

正常に動作していますね。このように、伝統的なマクロの問題点は健全なマクロを使うと回避することができます。

●識別子の使い方

識別子を定義すると、それをキーワードとして利用することができます。たとえば、cond や case には else 節がありますが、else を識別子として定義すると、それをキーワードとしてマクロ定義に使うことができます。

簡単な例として、if に then と else というキーワードを追加してみましょう。マクロ名は my-if とします。簡単な使用例を示します。

gosh[r7rs.user]> (my-if #t then 'OK else 'NG)
OK
gosh[r7rs.user]> (my-if #f then 'OK else 'NG)
NG
gosh[r7rs.user]> (my-if #t then 'OK)
OK
gosh[r7rs.user]> (my-if #f else 'NG)
NG

プログラムは次のようになります。

リスト : if - then - else のマクロ定義

(define-syntax my-if
  (syntax-rules (then else)
    ((_ test then e1 else e2) (if test e1 e2))
    ((_ test then e1) (if test e1 #f))
    ((_ test else e1) (if test #f e1))))

syntax-rules の次のリストに、識別子 then と else を定義します。パターンに識別子が含まれている場合、識別子は自分自身とマッチングします。最初のパターンは then と else の両方がある場合で、(if test e1 e2) に変換します。次のパターンは then だけがある場合で、(if test e1 #f) に変換します。最後のパターンは else だけがある場合で、(if test #f e1) に変換します。

●マクロの再帰定義

健全なマクロは再帰定義することもできます。簡単な例として and をマクロ定義してみましょう。次のリストを見てください。

リスト : and のマクロ定義

(define-syntax my-and
  (syntax-rules ()
    ((_) #t)
    ((_ a) a)
    ((_ a b ...) (if a (my-and b ...) #f))))

マクロ名は my-and とします。最初のパターンは引数がない場合とマッチングします。この場合、テンプレートは #t になります。次のパターンは引数が一つの場合とマッチングします。この場合、引数 a の評価結果が my-and の返り値になるので、テンプレートは a になります。

最後のパターンは引数が 2 個以上の場合とマッチングします。第 1 引数はパターン変数 a とマッチングし、第 2 引数は b とマッチングします。残りの引数は省略子 ( ... ) とマッチングします。このように、省略子を使うと可変個の引数を取るマクロを定義することができます。テンプレートは (if a (my-and b ...) #f) です。if の then 節で my-and を再帰呼び出しします。これで、b と残りの引数に対して my-and のマクロ展開が行われます。なお、パターン引数 b を省略するとエラーになります。ご注意ください。

それでは実行してみましょう。

gosh[r7rs.user]> (my-and)
#t
gosh[r7rs.user]> (my-and 1)
1
gosh[r7rs.user]> (my-and 1 2)
2
gosh[r7rs.user]> (my-and 1 2 3)
3
gosh[r7rs.user]> (my-and 1 2 #f 3)
#f

正常に動作していますね。

●健全なマクロの弱点

ところで、健全なマクロは万能ではなく、syntax-rules だけでは簡単に実現できない場合もあります。たとえば、Common Lisp の block のように、評価中の S 式から脱出して値を返すマクロを考えてみましょう。

Common Lisp の block は return-from tag-name で block から脱出するのですが、今回は tag-name を評価すると block から脱出することにします。この処理は継続を使うと簡単です。プログラムは次のようになります。

リスト : マクロ block の定義

(define-syntax block
  (syntax-rules ()
    ((_ tag e1 ...)
     (call/cc
       (lambda (tag) e1 ...)))))

(block tag e1 ...) を (call/cc (lambda (tag) e1 ...)) に変換するだけです。これで tag を評価すると block から脱出することができます。簡単な実行例を示します。

gosh[r7rs.user]> (block return (do ((x 0 (+ x 1))) ((>= x 10)) (if (< 5 x) (return #f) (display x))))
012345#f

それでは、tag-name の指定を省略して、block の中で return が評価されたら脱出するようにプログラムを修正してみましょう。簡単だと思われるかもしれませんが、次のようにプログラムしても動作しません。

リスト : マクロ block1 の定義 (間違い版)

(define-syntax block1
  (syntax-rules ()
    ((_ e1 ...)
     (call/cc
       (lambda (return) e1 ...)))))

ラムダ式の引数 return は自由変数なので、健全なマクロは「変数捕捉」が起きないように S 式をマクロ展開します。このため、block の中では return という名前で継続にアクセスすることができなくなるのです。実際に実行すると次のようになります。

gosh[r7rs.user]> (block1 (do ((x 0 (+ x 1))) ((>= x 10)) (if (< 5 x) (return #f) (display x))))
*** ERROR: unbound variable: return

このような場合は伝統的なマクロを使うとうまくいきます。プログラムは次のようになります。

リスト : マクロ block1 の定義

(define-macro (block1 . args)
  `(call/cc (lambda (return) ,@args)))

伝統的なマクロは S 式を単純に置換するだけなので、block の中では return という名前で継続にアクセスすることができます。それでは実行してみましょう。

gosh> (block1 (do ((x 0 (+ x 1))) ((>= x 10)) (if (< 5 x) (return #f) (display x))))
012345#f

正常に動作していますね。伝統的なマクロの弱点である「変数捕捉」も、使い方によっては役に立つこともあります。伝統的なマクロの使い方は、Common Lisp ですが下記参考文献で詳しく説明されています。マクロに興味のある方は一読することをお勧めします。

●参考文献


改訂 2009 年 6 月 6 日
改訂 2020 年 9 月 12 日

Copyright (C) 2009-2020 Makoto Hiroi
All rights reserved.

[ PrevPage | Scheme | NextPage ]