Island Life

< practical-scheme.net OS移行 | Waldstein >

2014/12/01

Gaucheの低レベルマクロ機構

(Lisp Advent Calendar 2014参加エントリ)

まだ不完全だが、GaucheのHEADにExplicit renaming macro (er-macro)を入れた。 これから徐々にレガシーマクロ (define-macro) で定義されているマクロを 置き換えてゆく予定。 (ただ、0.9.4ではdefine-syntaxフォームをプリコンパイルできないので、 組み込みマクロを本格的に置き換えるのは0.9.5リリース以降になる。)

低レベルマクロ、つまりマクロ展開にScheme自身を使えるマクロシステムは いくつも提案されている。R6RSにはそのうちのひとつであるsyntax-caseが入った。 syntax-case マクロはパターンマッチによる 入力フォームの分解が使えるし、出力も特に凝ったことをしなければ syntax で囲むだけなので、使い勝手だけを考えるならおそらく最良だ。

例えばふたつの変数の値を入れ替える swap! マクロは syntax-case ではこうなる。

(define-syntax swap! 
  (lambda (stx) 
    (syntax-case stx () 
      ((_ a b) 
       (syntax 
        (let ((value a)) 
          (set! a b) 
          (set! b value))))))) 

一時変数valueは自動的にリネームされるので、 マクロ呼び出し側で (swap! value x) のように呼び出しても干渉することはない。 またマクロ展開結果に挿入されるletおよびset!も、 マクロ定義時に見えている束縛 (ここではグローバルな束縛が見えているので、 Schemeでの標準的な意味) を参照するようになっているので、次のように マクロ使用環境でset!等が別の意味に束縛されていても問題ない。

(let ((set! list))
  (swap! x y))

さてこのswap!をer-macroで書くとこうなる。

(define-syntax swap!
  (er-macro-transformer
   (^[f r c]
     (let ([a (cadr f)]
           [b (caddr f)])
       `(,(r'let) ([,(r'value) ,a])
          (,(r'set!) ,a ,b)
          (,(r'set!) ,b ,(r'value)))))))

行数こそさほど変わらないものの、syntax-case に比べかなり面倒くさい。 何をやっているかというと、

  • fにはマクロ呼び出しフォーム全体がS式で渡ってくる。
  • マクロ引数のaやbにあたる式は自力で取り出す必要がある。
  • 出力を組み立てる際に、マクロが挿入する識別子はすべてrename手続き (上のコードではr) を通して陽にリネームする。 リネームというのは若干誤解を招くんだけど、letset! は 単にシンボルの名前を変えるんじゃなくて、どうにかして「トップレベルで見えているletset!を確実に参照できる名前」になる、と考える。

いちいちこんなふうにマクロを書くのは実用上は大変面倒なのだけれど、 「マクロが何をやっているかが明示されている」という意味では もっとも明確だ。Gaucheがer-macroを低レベルマクロの一番の基礎に 置こうとしているのは、動作が目に見えるプリミティブな部品をまず用意して、 便利な機能はそれを組み立てて作る、と考えているからである。

実際、入力フォームの分解部分はutil.matchを使えばパターンマッチでいけるし、 出力の組み立て部分についても、「マークされたもの以外の識別子を全部renameする」 といった上位のユーティリティを提供して、実用コードはそれらを使って書いてもらう予定。

syntax-caseでは、パターンマッチ部分が 「環境情報でラップされた入力フォームを必要なだけアンラップする」という 動作と密都合されてしまっているので、util.matchのような 外部のS式マッチャを使うことは出来ないし、syntax-caseのパターンマッチャ部分だけを 取り出して普通の手続きの中で使うこともできない。 このせいで、syntax-caseは、まるで 汎用的なレゴの中に一個だけ巨大な単一用途のブロックが混ざってくるような 感じになってしまっている。それがGaucheでsyntax-caseを避けた理由。

★ ★ ★

ところで、上で 「トップレベルで見えているletset!を確実に参照できる名前」に リネームされる、と書いたが、具体的にどうリネームされるのだろうか。 実は、rename手続きだけで頑張っても無理で、処理系自体の何らかのサポートが必要になる。 Schemeの衛生マクロ(hygienic macro)が何となくつかみ所が無いのは、 このへんが処理系に任されていて、具体的なイメージを持ちにくいからだろう。

例えば処理系が、トップレベルの束縛についてはletset!等の 単純な名前だけでなく、モジュール名を付加した scheme.base/letscheme.base/set! のような別名を用意するようになっていれば、 ここでのrename手続きはletscheme.base/let にリネームする、 といった動作になる。

Clojureのバッククオートでリテラルシンボルがnamespace qualifiedなシンボルに 変換されるのは、まさに上記のようなリネームをやっているわけだ。ローカルマクロが 無い場合、マクロが挿入する自由変数はすべてトップレベルへの参照になるので、 それで十分なのだ。(ローカルマクロが入ってくると、トップレベルだけでなく マクロ定義の外側にあるローカル変数を参照するケースがありえるので、 Clojure方式はそのままでは応用できない。)

Gaucheでは、シンボルをidentifierというオブジェクトでラップして、 モジュールや環境情報を付加することで擬似的にリネームを実現している。

Tags: Programming, Gauche, Macro

Post a comment

Name: