lisp-unit.lisp

http://www.cs.northwestern.edu/academics/courses/325/readings/lisp-unit.html
xyzzy-lisp 用の test tool が欲しいな、作ってみようかな。つーか common-lisp 用のならあるんじゃね?と探してみて見つけたのが lisp-unit.lisp
ざっと見た感じ xyzzy-lisp に移植するのは簡単そうなんだけど、とりあえず読んでみて面白いなと思った部分について書いておく。といってもわかりやすく説明できるかまことに不安だが。

大雑把な流れ

(define-test add
  (assert-equal 3 (add 1 2))
  (assert-equal 5 (add 2 3)))

define-test で assert-* する tests を定義しておく。これは *package* に関連付けられて *tests* に保存される。

(run-tests add)

run-tests で指定した名前の tests を実行(というのだろうか?)する。

問題: tests を定義するときに macro を展開してはいけない

test を保存しておくっていうとぱっと思いつくのは

(define-test add
  (assert-equal 3 (add 1 2))
  (assert-equal 5 (add 2 3)))

なら

(lambda ()
  (if (equal 3 (add 1 2)) <pass> <fail>)
  (if (equal 5 (add 2 3)) <pass> <fail>))

みたいな関数を作っておいて、run-tests されたら funcall するとかいう方法なんだが、使い方として

  1. 何かに対する tests を書く
  2. 何かを実装
  3. 何かに対する tests を実行
  4. pass するまで 2-3 を繰り返す

というような使い方を考えると、テスト対象の何かが macro だった場合に tests を書いた時点で macro を展開して保存しちゃうと、何かを書き換えても(展開されて保存された)tests のコードは古い macro 定義に従って展開されたままになってしまう。

つまり

(defmacro add (a b)
  `(* ,a ,b)) ;間違ってるのはわざと

なんて定義だった場合に、関数を作っちゃうとその時点で macro は展開されるので

(lambda ()
  (if (equal 3 (* 1 2)) <pass> <fail>)
  (if (equal 5 (* 2 3)) <pass> <fail>))

という関数になってしまって、後から add の定義を修正しても test は間違った展開系のままになってしまう。

解決法: sexp を list として保存しておく

lisp-unit.lisp では *tests* に define-test された tests を保存してるのだけど、その時に保存するのは

(add ((assert-equal 3 (add 1 2)) (assert-equal 5 (add 2 3))))

という list で、これを run-test するときに拾ってきて cdr 部分を lambda 式の sexp に埋め込み、function に coerce する、ということをしてた。

;; この式はイメージです
(coerce `#'(lambda () ,@(cdr test)) 'function)

そうすると run-tests を呼び出すまでは (assert-equal 3 (add 1 2)) という list の形がキープされて run-tests を呼び出して始めて add を展開するので、add の定義を書き換えるとちゃんと新しい定義で評価される。なるほどねぇ。


おまけ: thunk???

なんか test の code を list として保存しておいて、function に coerce したものを 'thunk' って呼んでる。get-test-thunk とか run-test-thunk とか。
で thunk というのを辞書で引いてみたけど、どうもしっくり来るような意味は無さそうだ。まさか chunk の typo?とか思いつつググったら
thunkって? - higepon blog
へーへーへー。