Island Life

< らむ太とRoomba | Mystery and recovery stories >

2010/04/25

Schemeのマクロ

これはツッコミを誘っているのかな :-) まあ、Schemeのマクロについては仕様書だけを見てもわけわからんのは 確かなので、ちょっと解説してみる。引用は全て上のリンクから。

まず、「衛生的マクロ (健全なマクロ)」と「R5RS/R6RSのマクロ」を混同しないように 気をつけよう。衛生的マクロは何らかの方法で名前の衝突の回避をマクロ側がサポートする しくみで、syntax-rulesやsyntax-caseはその一つの実装 にすぎない。

上のリンク先の文章中の「Schemeの衛生的マクロは…」の部分はほぼ全て、 「Schemeのsyntax-rulesは…」と置き換えて読むのが良いだろう。

単にANSI Common Lispのマクロは衛生的マクロより強力だ、ってだけの話です。

CLのマクロはsyntax-rulesよりも強力。でも他の衛生的マクロよりは非力。

なぜR5RSのマクロとしてsyntax-rulesしか入っていないかというと、 それはsyntax-rulesが衛生的マクロとして最良だったから、ではなくsyntax-rulesより強力な衛生的マクロとしていくつもある候補のどれを採用するかで意見が一致しなかったから、 というのが真相だったりする。

衛生的マクロの(syntax-rules () ...)に関して言うと、僕もこれが何の為にあるんだか、 ぶっちゃけ分からないです(笑)。これしかないんだったら、 明示せんでもエエんちゃうの?とか思ってるんですが (実際は、R6RSにはsyntax-caseと言うものもあり)。 まあ、恐らく、Schemeのλ式による手続き定義と対応させるためだけに入ってるんでしょうけどね。

上で書いたように、syntax-rules以外にも衛生的マクロはいくつもあり得る。 syntax-rulesフォームは、それ自体が「より低レベルなマクロ展開ルーチン」へと展開される マクロであることを意図している。

実際、R4RSの付録には、syntax-rulesの他に通常のlambda式でマクロエキスパンダが 書けるマクロが提示されていた。つまり、パターン言語などではなくScheme自体で マクロが書ける。syntax-rulesも単なるマクロで、展開されるとマクロエキスパンダ手続きに なるという寸法だ。

ただ、R4RSの低レベルマクロはいまいち筋が悪かった。 例えばwhenマクロはこんな感じ。

;; R4RS付録のlow-levelマクロ
(define-syntax when
  (lambda (form)
    (let* ((args (unwrap-syntax (cdr (unwrap-syntax form))))
           (test (car args))
           (body (cdr args)))
      `(,(sytnax if) ,test (,(syntax begin) ,@body) #f))))

CLのマクロと違うのは以下の3点。

  1. 引数に元の式が丸ごと渡ってくるので、testとbodyを分解してやらないとならない。
  2. さらに、引数に渡ってくる式にはレキシカルな環境情報がくっついてくるので、 いちいちunwrap-syntaxでそいつをとっぱらってやる必要がある。
  3. マクロの出力に「生の」シンボルを挿入するとエラーになる。 いちいちsyntaxで構文情報をつけてやらないとならない。

これはさすがに面倒くさい。そのせいか、 R5RSでマクロを正式に採用するにあたって却下されてしまった。

(R6RSで入ったsyntax-caseは上の1,2をパターンマッチングで、 3をquasisyntaxで解決していると言える。)

よりわかりやすい衛生的マクロの例としては、explicit renamingというのがある。 もし処理系のネイティブのマクロシステムがexplicit renamingならば、 不健全なwhenマクロはこう書けるだろう。

(define-syntax when
  (lambda (form rename compare)
    (let ((test (cadr form))
          (body (cddr form)))
      `(if ,test (begin ,@body)))))

formにマクロフォーム全体が渡ってくるので自分でtestとbodyに分解して やらないとならないが、リストを組み立てて返せばいいのはCLのマクロと同じだ。

(Gaucheはexplicit renamingマクロをサポートしてないので、上の式は実行できない。 また、処理系によってはネイティブの低レベルマクロがexplicit renamingであるとは 限らないので、一段フォームを噛ましてやることが多い (そうすれば他の形式へと 変換できるので)。MIT-Schemeではこんな感じ。

(define-syntax when
  (er-macro-transformer 
    (lambda (form rename compare)
       ...)))

)

さて、上のexplicit renamingマクロは不健全であると書いた。 なぜかというと、マクロが使われる環境でもし'if'や'begin'が束縛されていると 誤動作するからだ。

(let ((if list))
  (when #f (write "oops") 'a))

  ↓ whenを展開

(let ((if list))
  (if #f (begin (write "oops") 'a)))

  ↓ このifはレキシカルに束縛されている!

(list #f (begin (write "oops") 'a)) と同じことなので、
"oops" が出力されて、(#f a) が帰る。

実は、これはCLのマクロでも同じである。

(defmacro %when (test &body body)
  `(if ,test (progn ,@body)))

(macrolet ((if (&body xs) `(list ,@xs)))
  (%when nil (write "oops") 'a))

=> "oops" が出力されて (nil a) が帰る。

つまり、CLでは変数衝突を完全に避けるマクロを書くことはできない。 単に、「macroletでifなんて紛らわしいローカルマクロ定義するやつおらんだろう」 ということをあてにしているだけなのだ。 (Allegro CLだと上のような式をコンパイルしたら警告だしてくれるけど。)

まあ、それでもCLでは困ってないわけで、実用上、ifだのlambdaだのが シャドウされる危険ってのは無視してもだいたい大丈夫なのかもしれない。 もっともSchemeの場合、Lisp-1であるせいで、CLにはない、 ローカル変数が標準関数をシャドウしちゃう危険がある。マクロが関数listへの 呼び出しを埋め込んでて、使われる環境でlistなんていうローカル束縛があると まずいわけだ。

上のexplicit renaming macroによる定義を次のように変えると、 健全(衛生的)なマクロになる。

(define-syntax when
  (lambda (form rename compare)
    (let ((test (cadr form))
          (body (cddr form)))
      `(,(rename 'if) ,test (,(rename 'begin) ,@body)))))

renameにはシンボルを取る手続きが渡ってくる。renameの返り値は、 「このマクロが定義された時点での、ifやbeginが指すものと同じものを指す、別の識別子」だ。 他と衝突しない別名が自動的につけられる、と思えばいいだろう。 具体的にそれが何か、は処理系が提供するメカニズムによるので何とも言えないけど。


加えて、quasiquoteはR5RSでも定義されているが、マクロが衛生的マクロしか定義されてないので、仕様書範囲内では何のために存在してるんだかサッパリ、である。単純にそれこそlistの代用としてしか使い道がない。仕様書の範囲内では。

歴史的にはbackquoteはマクロのために導入されたのかもしれないけど、 現代のLispではbackquoteはマクロと独立した 単なるテンプレートの仕組みと考えられてるんじゃないかなあ。 だから別にR5RSにあってもおかしくない。 たとえばGaucheのソースを見てもらえば、マクロ以外でもquasiquoteは大量に 使われてるのがわかると思う。 私がLispを知るきっかけとなった『CプログラムブックIII Lisp処理系の作成』 でも、確か「熟練したLispプログラマはlistなんて使いません。 リストを作る時はbackquoteを使います」って説明があったような気がする。


最後に実装依存ですが、

    「やっぱSchemeがいいなあ~~。」

と言う人の為に、いくつかのScheme処理系での伝統的マクロの使い方を紹介しておきます。

  • Gauche: デフォルトでdefine-macroと言う形式で利用可能です。
  • PLT Scheme: (require mzlib/defmacro)を評価した後、define-macroと言う形式で利用可能です。
  • Guile: デフォルトでdefine-macroと言う形式で利用可能です。

syntax-rulesは不自由すぎるのだけれど、それ以外の衛生的マクロ、 syntax-caseやexplicit renaming等はどれも力の上では同じであることがわかっていて、 ひとつがあれば別のやつはポータブルに実装できることがわかっている。 さらに、どれかひとつがあれば伝統的マクロもポータブルに実装できることもわかっている。

なので、原理的に言えばSchemeの伝統的マクロは(特定の実装にしかない 特殊なメカニズムに依存するという意味での)実装依存ではない。 確かに仕様の範囲内ではないけれど、R6RSの仕様だけで書ける処理ではある。

Tags: Scheme, Lisp

Post a comment

Name: