HOME もうひとつの Scheme 入門 書き込む

15. 構文の定義 (マクロ)


1. 初めに

今回は Lisp 語族に特徴的機能である、自前の構文を定義する方法(マクロ)について 説明します。マクロが定義できるようになると、プログラムがさらに簡潔に 書けるようになります。

マクロとは式の変換です。 式が評価される前に、または、コンパイル時に式が変換されます。 そして、変換後の式が初めからソースコードに書いてあったかのように処理が行われます。

Common Lisp のマクロ定義はかなり複雑ですが、R6RS に準拠した Scheme では syntax-rules という形式によって比較的簡単に定義できます。 syntax-rules を使うと変数補足などのわずらわしいことを気にしないで、 ”この式をこういう式に変換しろ”ということを直接的に書くことができます。

ただし、syntax-rules で記述できないマクロを書くのは Common Lisp より複雑になります。

2. 簡単なマクロの例

簡単な例を示して説明しましょう。 [code 1] は変数に '() を代入するマクロです。

[code 1]

(define-syntax nil!
  (syntax-rules ()
    ((_ x)
     (set! x '()))))
syntax-rules の 2 番目の引数は、もとの式 → 変換後の式 を記述した組です。 また、_ はマクロ名を表します。 つまり、[code 1] の意味は、(nil! x) という式を (set! x '()) に変換しろということです。

これは関数で書くことはできません。関数で書くと、クロージャーの働きにより、関数の外の変数 と内部の変数は別の変数になり、関数が自分の外の変数を変化させることはできないからです。 試しに [code 1] の関数版を書いてどうなるか見てみましょう。

[code 1']

(define (f-nil! x)
   (set! x '()))
> (define a 1)
a

> (f-nil! 'a)
a

a
1           ; a の値は変わらない

> (nil! a)
1

> a
()          ; a が '() になった。

もうひとつ簡単な例を示しましょう。predicate が満たされるとき複数の式が実行されるマクロ when を書いてみましょう。

[code 2]

(define-syntax when
  (syntax-rules ()
    ((_ pred b1 ...)
     (if pred (begin b1 ...)))))
[code 2] で出てきた ... は 0 個を含む任意個の式を表します。 [code 2] の意味は、
(when pred
  b1
  ...)
(if pred
  (begin
     b1
     ...))
に変換するということです。 これも、特殊形式 if に変換されるマクロですから、関数で書くことはできません。使用例は以下のようになります。
(let ((i 0))
  (when (= i 0)
    (display "i == 0")
    (newline)))
i == 0
;Unspecified return value
簡単なマクロの実用的なものとして while と for を挙げておきます。while は predicate が成り立つ間 本体を実行し、for は数を表す変数がある範囲内にある間処理を実行します。

[code 3]

(define-syntax while
  (syntax-rules ()
    ((_ pred b1 ...)
     (let loop () (when pred b1 ... (loop))))))


(define-syntax for
  (syntax-rules ()
    ((_ (i from to) b1 ...)
     (let loop((i from))
       (when (< i to)
	  b1 ...
	  (loop (1+ i)))))))
実行例を以下に示します。
(let ((i 0))
  (while (< i 10)
    (display i)
    (display #\Space)
    (set! i (+ i 1))))
0 1 2 3 4 5 6 7 8 9 
;Unspecified return value

(for (i 0 10)
  (display i)
  (display #\Space))
0 1 2 3 4 5 6 7 8 9 
;Unspecified return value

練習問題 1

ある条件が満たされないとき複数の式を評価するマクロを作ってください。(when の反対です。)

3. syntax-rules の高度な使い方

3.1. 複数のパターンを定義する。

syntax-rules には複数の変換パターンを定義することができます。例えば、変数の値を増加させるマクロ incf を 考えて見ましょう。変数名だけ与えられたときは 1 増やし、変数名と増分が与えら得れたときには増分だけ増やすようにします。 [code 4] のように複数の変換パターンを記述することによって対応することができます。

[code 4]

(define-syntax incf
  (syntax-rules ()
    ((_ x) (begin (set! x (+ x 1)) x))
    ((_ x i) (begin (set! x (+ x i)) x))))
> (let ((i 0) (j 0))
  (incf i)
  (incf j 3)
  (display (list 'i '= i))
  (newline)
  (display (list 'j '= j)))
(i = 1)
(j = 3)

練習問題 2

変数から減少分を引くマクロ decf を作ってください。減少分が省略されて時は 1 を引いてください。

練習問題 3

[code 3] の for を改良して、ステップ幅を指定できるようにしてください。 ステップ幅が省略されたときは 1 になるようにしてください。

3.2. マクロの再帰的な定義

or, and はマクロで、以下のように再帰的に定義されています。マクロ定義も再帰的に定義できるので、 かなり複雑な構文を定義することができます。

[code 5]

(define-syntax my-and
  (syntax-rules ()
    ((_) #t)
    ((_ e) e)
    ((_ e1 e2 ...)
     (if e1
	 (my-and e2 ...)
	 #f))))

(define-syntax my-or
  (syntax-rules ()
    ((_) #f)
    ((_ e) e)
    ((_ e1 e2 ...)
     (let ((t e1))
       (if t t (my-or e2 ...))))))

練習問題 4

let* を定義してください。

3.3. 予約語の使用

syntax-rules の最初の引数はマクロ内で使用する予約語のリストです。例えば、cond は [code 6] のように定義されます。[code 6] で else は 予約語として働きます。

[code 6]

(define-syntax my-cond
  (syntax-rules (else)
    ((_ (else e1 ...))
     (begin e1 ...))
    ((_ (e1 e2 ...))
     (when e1 e2 ...))
    ((_ (e1 e2 ...) c1 ...)
     (if e1 
	 (begin e2 ...)
	 (cond c1 ...)))))

4. 局所マクロ

Scheme では、let-syntax, letrec-syntax を使って 局所的に構文を定義することができます。使い方は通常の define-syntax とほぼ同じです。

5. syntax-case

syntax-rules で全てのマクロが記述できるわけではありません。そのようなマクロを記述するための方法が R6RS のライブラリの syntax-case に定義されています。
syntax-case をつかうと Common Lisp の マクロと同等のものをより安全に定義することができます。 syntax-case は難易度が高いので、はじめは飛ばしてもしてもかまいません。

以下に比較的簡単な例を挙げます。

[code 7]


 
(define-syntax show-vars
  (lambda (x)
    (syntax-case x ()
      [(_) #''shown]
      [(_ e1 e2 ...) 
       #'(begin (display 'e1) (display "->") (display e1) (newline) (show-vars e2 ...))])))


 
(define-syntax aif
  (lambda (x)
    (syntax-case x ()
      [(k c b ...)
       (with-syntax 
	([it (datum->syntax #'k 'it)])
	#'(let ((it c))
	    (if it b ...)))])))


最初のマクロ show-vars は変数の値を表示するマクロです。以下のように使います。
> (let ((i 0) (j 1) (k 2)) (show-vars i j k))
i->0
j->1
k->2
shown
2 番目の aif は代名詞マクロです。predicate の結果を it として参照できます。 datum->syntax の2番目の引数を 'it とすることで代名詞 it を意図的に捕捉しています。 使用例は以下の通りです。
> (let ((i 4))
  (aif (memv i '(2 4 6 8))
       (car it) #f))
4

6. 終わりに

Scheme のマクロについて簡単に説明しました。 マクロを使わなくてもプログラムは書けますが、マクロを使ったほうがエレガントなプログラムが書けます。

Common Lisp ではマクロを書くにはそれなりの熟練が必要ですが、 Scheme の syntax-rules を使うと比較的簡単にマクロを書くことができます。

syntax-case を使うと、Common Lisp で定義できるマクロのほとんどをより安全に定義することができます。 ただ、syntax-case はかなり難易度が高いです。

練習問題の解答

練習問題 1

(define-syntax unless
  (syntax-rules ()
    ((_ pred b1 ...)
     (if (not pred)
	 (begin
	   b1 ...)))))

練習問題 2

(define-syntax decf
  (syntax-rules ()
    ((_ x) (begin (set! x (- x 1)) x))
    ((_ x i) (begin (set! x (- x i)) x))))

練習問題 3

(define-syntax for
  (syntax-rules ()
    ((_ (i from to) b1 ...)
     (let loop((i from))
       (when (< i to)
	  b1 ...
	  (loop (1+ i)))))
    ((_ (i from to step) b1 ...)
     (let loop ((i from))
       (when (< i to)
	  b1 ...
	  (loop (+ i step)))))))

練習問題 4

(define-syntax my-let*
  (syntax-rules ()
    ((_ ((p v)) b ...)
     (let ((p v)) b ...))
    ((_ ((p1 v1) (p2 v2) ...) b ...)
     (let ((p1 v1))
       (my-let* ((p2 v2) ...)
		b ...)))))