define-syntax でユニットテスト

結城さんが define-syntax を使った debug マクロ(デバッグプリント - 結城浩のSICP日記 - sicp)を紹介されている。マクロの便利な使い方の好例だと思う。Scheme ではなく、Common Lisp の話になってしまうが、高い評価を受けている Practical Common Lisp という本(オンラインで読める)にもマクロを使った面白い例が 9 章に載っている。その章ではマクロを使ってユニットテストのための簡易ライブラリを作っていくのだが、まだ Lisp のマクロというものがよく分からなかった僕はこの内容にとても感銘を受けた。せっかくなのでこの内容の前半をさらっと簡単に Scheme (Gauche) を使って紹介したいと思う。

本ではまずつぎのようなテスト(関数 + のテスト)を例にしている。

(= (+ 1 2) 3)
(= (+ 1 2 3) 6)
(= (+ -1 -3) -4)

これらすべてが #t ならばテストはパスするので、次のような関数を定義する。

(define (test-+)
  (and (= (+ 1 2) 3)
       (= (+ 1 2 3) 6)
       (= (+ -1 -3) -4)))

もちろん (test-+) の評価結果は #t である。

次に、それぞれのテストを分かりやすく表示するようにする。

(define (test-+)
  (define (boolean->string bool) (if bool "pass" "FAIL"))
  (format #t "[~A] ... ~A~%" (boolean->string (= (+ 1 2) 3)) '(= (+ 1 2) 3))
  (format #t "[~A] ... ~A~%" (boolean->string (= (+ 1 2 3) 6)) '(= (+ 1 2 3) 6))
  (format #t "[~A] ... ~A~%" (boolean->string (= (+ -1 -3) -4)) '(= (+ -1 -3) -4)))

かなり冗長だけど、結果はわかりやすくなる。

gosh> (test-+)
[pass] ... (= (+ 1 2) 3)
[pass] ... (= (+ 1 2 3) 6)
[pass] ... (= (+ -1 -3) -4)
#<undef>

重複する部分を関数にまとめておく。

(define (report-result result form)
  (format #t "[~A] ... ~A~%" (if result "pass" "FAIL") form))

(define (test-+)
  (report-result (= (+ 1 2) 3) '(= (+ 1 2) 3))
  (report-result (= (+ 1 2 3) 6) '(= (+ 1 2 3) 6))
  (report-result (= (+ -1 -3) -4) '(= (+ -1 -3) -4)))

やっている内容は同じなので、(test-+) の出力は先ほどと変わらない。しかし、やはりテストしたい式と、それを出力するときにつかう quote された式の二つを書かなければならないので何かと厄介だ。

ここでマクロの出番。check マクロを導入して、テストしたい式が評価される前に式そのものを quote できるようにする。

(define-syntax check
  (syntax-rules ()
    ((_ form)
     (report-result form 'form))))

(define (test-+)
  (check (= (+ 1 2) 3))
  (check (= (+ 1 2 3) 6))
  (check (= (+ -1 -3) -4)))

かなり見通しが良くなった。

gosh> (test-+)
[pass] ... (= (+ 1 2) 3)
[pass] ... (= (+ 1 2 3) 6)
[pass] ... (= (+ -1 -3) -4)
#<undef>

動作も問題ないようだ。

もう少し check マクロを工夫して、複数の式を同時に扱えるようにする。

(define-syntax check
  (syntax-rules ()
    ((_ form)
     (report-result form 'form))
    ((_ form1 form2 ...)
     (begin
       (report-result form1 'form1)
       (check form2 ...)))))

(define (test-+)
  (check
   (= (+ 1 2) 3)
   (= (+ 1 2 3) 6)
   (= (+ -1 -3) -4)))

最初のバージョンに比べるととても簡潔になった。まさにマクロならではといえる。

と、以上で紹介は終わり。しかし、さきの Practical Common Lisp ではマクロをどのように使うかをこの先もステップを踏んで丁寧に説明していく。語り口もうまいので引き込まれてしまう。Common Lisp のマクロシステムは define-syntax のそれとはかなり異なるが、それでもこの本から学べることは多いはず。Scheme な方にもぜひ読んでもらいたいと思い、簡単ではあるけど、今回ブログで紹介してみた。

追記: shiro さんよりコメントを頂きました。最後の check マクロはもっと単純に書けます。