読者です 読者をやめる 読者になる 読者になる

Clojureでsyntactic closureを使ってhygienic macroを書くためのライブラリを作りました

という話。当初は実現可能性を示すことを目的にしていましたが、それなりに実用できそうな感じになってきたのでライブラリとしてまとめました。

hygienic macroとは? syntactic closureとは?

詳細な説明はよそへ譲りますが、大雑把にいうと、hygienic macroとは識別子の衝突を自動的に回避する仕組みをもったマクロのことをいいます。hygienic macroについての研究は主にSchemeに対して行われていて、R5RSやR6RSに取り入れられているsyntax-rulesやsyntax-caseもhygienic macroの実装のひとつです。
syntactic closureはhygienic macroの別の実装で、hygienic macroの中ではもっとも単純なもののひとつです。
hygienic macroやsyntactic closureについての文献は少ないですが、それらについて書かれたウェブページとしてはこのあたりが参考になるかもしれません。

hygienic macroとCommon Lispのマクロのようないわゆる「伝統的なマクロ」で表現力を比較すると、syntax-case等によってCommon Lispのdefmacroが定義できる一方、hygienic macroでは、レキシカルスコープを"曲げる"ような、伝統的なマクロでは不可能なことができるため、hygienic macroの方が伝統的なマクロより表現力が高いといえます。

Clojureのマクロシステム

Clojureのマクロシステムは伝統的なマクロをベースとしたもので、hygienic macroではありません。しかし、syntax-quoteを使うことでほとんどの場合で識別子の衝突を引き起こさないマクロを書くことができます。

syntax-quoteは、Schemeのquasiquoteのようにマクロのテンプレートとして使うことができますが、それに加えて次のことをリード時に実行します。

  • シャープ(#)で終わるシンボルをgensymで生成したシンボルに置き換える(auto-gensym)
  • それ以外のシンボルを名前空間で修飾する
user=> `(let [x# 42] (* x# x#))
(clojure.core/let [x__490__auto__ 42] (clojure.core/* x__490__auto__ x__490__auto__))
user=> 

これにより、実用的な範囲ではほぼhygienic macroと同じことが可能ですが、上で触れたようなレキシカルスコープを曲げるような芸当は(少なくとも素直には)できないので、Clojureのマクロシステムはhygienic macroより表現力が低いように見えます。このあたりの詳細についてはそのうち別エントリで書きたいと思います。

また、auto-gensymは便利ですが、限界もあって万能ではありません。以下の例を見ると分かるとおり、

user=> [`(x# x#) `(x# x#)]
[(x__497__auto__ x__497__auto__) (x__498__auto__ x__498__auto__)]
user=>

auto-gensymでは、シャープで終わるシンボルは同じ名前であっても別のsyntax-quoteに現れれば別のgensymに置き換えられます。したがって、たとえば

(defmacro my-doto [x & forms]
  `(let [obj# ~x]
     ~@(for [[method & args] forms]
         `(~method obj# ~@args))
     obj#))

というようにマクロを定義すると、2行目と5行目のobj#と4行目のobj#は別々のシンボルに展開されてしまいます。

(macroexpand '(my-doto (java.util.HashMap.) (.put "a" 1) (.put "b" 2)))
;=> (let* [obj__535__auto__ (java.util.HashMap.)] (.put obj__534__auto__ "a" 1) (.put obj__534__auto__ "b" 2) obj__535__auto__)

これを回避するには従来通りgensymを使う必要があり不便です。

このライブラリは何をしているか

前置きがだいぶ長くなりました。
上で書いたように、Clojureのマクロシステムでは、束縛変数の衝突回避(=auto-gensym)に難があったりするものの、自動的に名前空間を修飾することによる自由変数の衝突回避はうまく機能していそうです。

そこでこのライブラリでは、自由変数の衝突回避にはClojureの従来のマクロシステムと同じ自動的な名前空間の修飾を使いつつ、束縛変数の衝突回避にはSyntactic Closuresの元論文*1に沿って束縛変数の自動的なリネームを取り入れ、hygienic macroを実現しています。このあたりの実装の詳細についても、なんだかいろいろと怪しげなマクロのテクニックを使ったので、気が向いたら別エントリで書きます。

使い方

ライブラリのインストールはLeiningenを使っていれば、次をproject.cljの:dependenciesに追加し、lein depsするだけでできます。

[syntactic-closure "0.1.0"]

ライブラリを使う場合には、

(use 'syntactic-closure.core)

とします。これで、以下のようなsyntactic closureによるhygienic macroが定義できるようになります。

(define-syntax let1 [name init & body]
  (sc-macro-transformer
    (fn [env]
      (quasiquote
        (let [~name ~(make-syntactic-closure env nil init)]
          ~@(map #(make-syntactic-closure env [name] %) body))))))

macroexpandしてみると、束縛変数が自動的にリネームされるのが分かります。

(macroexpand '(let1 x 10 (* x x)))
;=> (let* [x403 10] (clojure.core/* x403 x403))

また、syntactic-closure.coreで提供されているScheme由来の関数やマクロは、名前が長かったり書き方が冗長だったりするので、名前空間syntactic-closureではこれらの略記版を提供しています。

(use 'syntactic-closure)

(defsyntax let1 [name init & body]
  (qq (let [~name ~^:? init]
        ~@^{:? name} body)))

略記法の詳細やその他使い方については、READMEサンプルコードを参照してください。

まとめ

ということで、syntactic closureを使ってClojureでもhygienic macroを定義するライブラリを書きました。とはいえ、自分でもいまいち「hygienic macroとは何ぞや」というところがすべて理解できているわけではないので、間違い等ありましたらご指摘いただけると助かります。

ちなみに1年ほど前にClojureでsyntax-rules(とsyntax-case)を使えるようにするライブラリというのが話題になったことがありましたが、あちらはマクロの入力フォームをパターンマッチして分解することに主眼を置いていて、hygienic macroが定義できるわけではないようです。

*1:ftp://publications.ai.mit.edu/ai-publications/pdf/AIM-1049.pdf