Let's start Scheme

2015-01-13

マクロの健全性または如何にして私はerマクロを使うのをやめsyntax-caseを愛するようになったか

メモワール(Memoir)という単語をTwitter上で見かけて、自分も何か(主にあほなこと)を書きたくなっただけ。

Schemeにはhygienic macroなるものがあり、清潔なマクロとか健全なマクロとか訳されるのであるが、それを使うと変数の衝突などの問題をあまり考えなくてもよくなるである。Scheme界では長年このマクロについて議論されてきたらしいのだが、R6RSで終に低レベルかつ健全なマクロの一つであるsyntax-caseが標準に入ることになった。これでマクロ展開時に式変形以上のことができると喜んだのもつかの間、R7RSではまた不便なsyntax-rulesだけに戻されてしまった。理由はよく分からない。風の噂ではR7RS-largeではExplicit Renamingと呼ばれる別の低レベルな健全マクロが入るとされている。

erマクロとはどんなものか?大まかに言えば、Common Lispのマクロに健全性を追加する機能を備えたものである。与えられた式の解析、どのシンボルを衝突しないものにするか等は全てユーザーに任される。どのような見た目になるのか、伝統的なaifをerマクロで作ってみることにした。
(define-syntax aif
  (er-macro-transformer
   (lambda (form rename compare)
     (let ((test (cadr form))
           (then (caddr form))
           (els  (if (null? (cdddr form))
                     #f
                     (cadddr form))))
       `(,(rename 'let) ((it ,test))
         (,(rename 'if) it ,then ,els))))))

(aif (assq 'a '((a . 0))) it)
;; -> (a . 0)
マクロの定義が与えられただけではどのような式を渡せば良いのか皆目検討もつかない。さらにはこのままでは期待する式が与えられなかった際のエラーもよく分からないものになる。
;; Oops!
(aif (ass 'a '((a . 0))))

#|
*error*
#<condition
  &compile
    program: (aif (assq 'a '((a . 0))))
    source: "macro.scm":17
  &assertion
  &who car
  &message pair required, but got ()
  &irritants ()

>
stack trace:
  [1] raise
  [2] caddr
  [3] #f
    src: (caddr form)
    "macro.scm":7
  [4] macro-transform
  [5] pass1
  [6] #f
  [7] with-error-handler
  [8] load
|#
きっちりやるのであればかなりのコードを書く必要があり、僕のようなずぼらな人間には使うのが辛いものである(もちろん式解析などパターンマッチでやってしまえば良いのではあるが...)。

ではsyntax-caseではどうだろう?同様のものは以下のように書ける。
(import (rnrs))

(define-syntax aif
  (lambda (x)
    (syntax-case x ()
      ((aif test then)
       #'(aif test then #f))
      ((aif test then else)
       (with-syntax ((it (datum->syntax #'aif 'it)))
         #'(let ((it test))
             (if it then else)))))))

(aif (assq 'a '((a . 0))) it)
;; -> (a . 0)

(aif (ass 'a '((a . 0))))
#|
*error*
#<condition
  &compile
    program: (aif (assq 'a '((a . 0))))
    source: "macro.scm":32
  &syntax
    subform: #f
    form: (aif (assq 'a '((a . 0))))
  &who aif
  &message invalid syntax

>
stack trace:
  [1] raise
  [2] macro-transform
  [3] pass1
  [4] #f
  [5] with-error-handler
  [6] load
|#
マクロ定義からどのような構文なのか分かりやすいし、エラーも構文がおかしいといってくれる(パターンを列挙する等もう少しエラーメッセージが詳しいとうれしいかもしれないが)。なにより、erマクロにはない必要な部分のみを記述しているという感じがよい。

僕にもsyntax-caseは人間が使うには複雑すぎる、erマクロのような式が直接いじれるマクロの方がいいと思っていた時期があった。しかしその思いは簡単なマクロを書くには問題ないのだが規模が大きくなればなるほど逆転していった。これは式解析、変形を記憶しておくことができる頭のいい人間用のマクロではないのではないかと。事実、入力された式の何番目にこの要素を期待してというようなおよそ僕の頭では処理しきれないような処理を負担さてくるではないか。確かにerマクロは直感的である。マクロとは単なるリスト操作であるということさえ分かっていればその使用感は単なるリスト操作プログラムとほぼ同等である。逆にsyntax-caseはwith-syntax等の新しい概念を要求するため初期投資はerマクロより大きいかもしれない。しかしながら、余計なことを考えなくてもよいというその払い戻しは計り知れない。特に僕のようなまずは目的を達すること、その後で手段を見直すような人間にはとても便利なツールである。

こうして僕はerマクロには別れを告げsyntax-caseを愛するようになった。

No comments:

Post a Comment