Let's start Scheme

2015-05-12

秘伝のタレマクロができるまで

Schemeでこのマクロ便利なんだけど、どうやったらこんな複雑なマクロ書けるんだ?と疑問に思ったことはないだろうか?ふと自分が書いているマクロが機能追加とともに巨大に(まだ小さいけど)になっていっているの(成長途中)をみて答えの一つを見た気がしたので駄文として残すことにした。

最初は小さいものだった


僕はテストはSRFI-64を使って書くことが多い。というかほぼ100%である。SRFI-64は便利なのだが、多値を扱うテストを書くAPIを標準で提供していない(と思う、真面目に仕様眺めてない、でかいし)。毎回let-valuesを書くのは馬鹿らしいので以下のようなマクロを書いた。
;; version 1
(define-syntax test-values
  (syntax-rules ()
    ((_ (expected ...) expr)
     (test-values 'expr (expected ...) expr))
    ((_ name (expected ...) expr)
     (test-equal 'expr (expected ...) (let-values ((results expr)) results)))))
非常に単純に多値をリストで受け取ってequal?で比較するだけだが、最初のうちはこれで十分だった。しかも、let-valuesを毎回書く必要もない上に、何のテストをしているのか(この場合は多値)というのが一目で分かるし、やっぱりマクロは便利だなぁ、程度で済んでいた。

リスト内の値を一つずつ比較したくなった


多値を返す手続きのテストを行っていると、どの値が失敗しているのかということが知りたくなる。そうするとリストにまとめて比較するのでは辛い部分が出てきたので以下のように変更した。
;; version 2
(define-syntax test-values
  (syntax-rules ()
    ((_ "tmp" name (e e* ...) (expected ...) (var ...) expr)
     (test-values "tmp" name (e* ...) (expected ... e) (var ... t) expr))
    ((_ "tmp" name () (expected ...) (var ...) expr)
     (let-values (((var ...) expr))
       (test-equal '(name expected) 'expected var)
       ...))
    ((_ (expected ...) expr)
     (test-values expr (expected ...) expr))
    ((_ name (expected ...) expr)
     (test-values "tmp" name (expected ...) () () expr))))
これなら返ってくる値を一つずつ比較するので、どの値が失敗するのかということが分かりやすくなった。マクロの量は2倍程度に膨らんだが、毎回test-equalを書く必要もないし楽だという感じであった。

予想される結果が複数ある場合が出てきた


このマクロはSASMを書いているときに使っているのだが、x86の一貫性のなさから返し得る値が複数ある場合が出てきた(例: ADD)。そうすると、何かを弄った拍子に結果が入れ替わるとかが起きると毎回テストを書き換えなければならない。それではテストを書いてる意味が薄れると思ったので、こういった場合にも対応できるようにした。
;; version 3
(define-syntax test-values
  (syntax-rules (or)
    ((_ "tmp" name (e e* ...) (expected ...) (var ...) (var2 ... ) expr)
     (test-values "tmp" name (e* ...) (expected ... e) 
                  (var ... t) (var2 ... t2)
                  expr))
    ((_ "tmp" name () (expected ...) (var ...) (var2 ...) expr)
     (let ((var #f) ...)
       (test-assert 'expr
                    (let-values (((var2 ...) expr))
                      (set! var var2) ...
                      #t))
       (test-values "equal" name (expected ...) (var ...))))
    ;; compare
    ((_ "equal" name () ()) (values))
    ((_ "equal" name ((or e ...) e* ...) (v1 v* ...))
     (begin
       (test-assert '(name (or e ...)) (member v1 '(e ...)))
       (test-values "equal" name (e* ...) (v* ...))))
    ((_ "equal" name (e e* ...) (v1 v* ...))
     (begin
       (test-equal '(name e) e v1)
       (test-values "equal" name (e* ...) (v* ...))))
    ((_ (expected ...) expr)
     (test-values expr (expected ...) expr))
    ((_ name (expected ...) expr)
     (test-values "tmp" name (expected ...) () () () expr))))
正直自分が書いたものじゃなければ見ただけでは理解できないレベルになりつつあるが、複数ある場合用のマクロ(test-values/altとか?)を作るのも嫌だなぁという気がしたのでこれはこれでいいかということになった。

単純なequal?では比較できない場合が出てきた


返ってくる値がequal?で比較できない、具体的には述語でテストしたい場合が出てきた。既にorで場合分けされているので同様にキーワードを追加すればいいだけだしということで追加した。
;; version 4
(define-syntax test-values
  (syntax-rules (or ?)
    ((_ "tmp" name (e e* ...) (expected ...) (var ...) (var2 ... ) expr)
     (test-values "tmp" name (e* ...) (expected ... e) 
                  (var ... t) (var2 ... t2)
                  expr))
    ((_ "tmp" name () (expected ...) (var ...) (var2 ...) expr)
     (let ((var #f) ...)
       (test-assert 'expr
                    (let-values (((var2 ...) expr))
                      (set! var var2) ...
                      #t))
       (test-values "equal" name (expected ...) (var ...))))
    ;; compare
    ((_ "equal" name () ()) (values))
    ((_ "equal" name ((? pred) e* ...) (v1 v* ...))
     (begin
       (test-assert '(name pred) (pred v1))
       (test-values "equal" name (e* ...) (v* ...))))
    ((_ "equal" name ((or e ...) e* ...) (v1 v* ...))
     (begin
       (test-assert '(name (or e ...)) (member v1 '(e ...)))
       (test-values "equal" name (e* ...) (v* ...))))
    ((_ "equal" name (e e* ...) (v1 v* ...))
     (begin
       (test-equal '(name e) e v1)
       (test-values "equal" name (e* ...) (v* ...))))
    ((_ (expected ...) expr)
     (test-values expr (expected ...) expr))
    ((_ name (expected ...) expr)
     (test-values "tmp" name (expected ...) () () () expr))))
最初は6行のマクロだったのに、現状では30行までに膨れ上がった。多分そのうち、返ってくるレコードの中身を調べる必要がある、ということが発生するのでまだ大きくなる予感がしている。

結局何が言いたいのか?


複雑怪奇なマクロというのは時として秘伝のタレ的に継ぎ足し継ぎ足しで大きくなっていく場合があるということである。今回はテストケース用APIの薄いラッパーなので、都度変えていくという選択肢もあったのだが、既に公開されているマクロを後方互換性を保ったまま拡張する必要がある場合などいかんともしがたい場面もあるのではないだろうか?

特になにという結論は用意していないのだが、巨大なマクロを見たら「お前にも歴史があったかもしれないんだねぇ」という目で見てみるのも面白いかもしれない。

No comments:

Post a Comment