チルダクオート

マクロを組むときには変数捕捉に気をつけないといけないが、実は変数捕捉をうまく使う方法もあって、On Lispにそんな例が載っている。
http://www.komaba.utmc.or.jp/~flatline/onlispjhtml/anaphoricMacros.html

一番簡単な例としては、アナフォリックIFがある。

(defmacro aif (test-form then-form &optional else-form)
  `(let ((it ,test-form))
     (if it ,then-form ,else-form)))

(aif (big-long-calculation)
  (foo it))

この例では、比較結果である変数itを捕捉し、aifのthen-form内で使えるようにする。

Clojureでは少し長くなって、

(defmacro aif
  ([test-form then-form]
    `(aif ~test-form ~then-form nil))
  ([test-form then-form else-form]
    `(let [~'it ~test-form]
       (if ~'it ~then-form ~else-form))))

(aif (big-long-calculation)
  (foo it))

である。*1

アナフォラのitの前にチルダクオート(~')が付いているが、これはClojureのバッククオートがCommon Lispのものとは違う動作をするため。
Common Lispのバッククオートは、アンクオートできるクオートである。Clojureのバッククオートは、これに「名前空間解決&付加」という機能がプラスされている。
素のシンボルが欲しければ、クオートされたシンボルをアンクオート(すなわちチルダクオート(~'))しないといけない。

 * `(hoge list ,'list)
(HOGE LIST LIST)
user=> `(hoge list ~'list)
(user/hoge clojure.core/list list)

この違いは、マクロを使うときの名前衝突への対策のため。ちなみに、各言語での名前衝突への対策はこんな感じ:

  • Common Lisp : そもそもLisp-2*2なので変数名と関数名は衝突しないよ派
    • #'とかfuncallとかうぜぇ。
  • Scheme(R4RS〜) : 名前の衝突を自動回避するならLisp-1*3でも大丈夫だよ派
    • アナフォリックマクロみたいな、意図的に安全対策を破る(orサボる)ようなマクロは書けない。
    • Gaucheとかはdefine-macroが使えるけど、名前の衝突に関しては自己責任の部分がすごく増える。
  • Clojure : バッククオートの定義を変えて、read時に名前空間を付け足してしまえばLisp-1でも大丈夫だよ派
    • バッククオートがマクロ以外の用途に(ほぼ)使えなくなった。
    • macroexpandするとほとんどのシンボルに名前空間がくっついてくるので激しく見づらい。でっかいマクロを展開すると、画面がclojure.coreだらけになる(涙目)。

Clojureでは、バッククオートの定義を変えてしまったために、アナフォラを導入するときにチルダクオート(~')が必要になってしまったけど、逆に安全対策を破っていることを示すサインみたいになっていて、かえって良いかも知れない。

*1:if-letがあるからこんなのいらないよという話は置いといて。

*2:変数の名前空間と関数の名前空間が別々なので衝突しない。(その代わり、名前空間を行き来するオペレータ(symbol-functionとかfuncallとか)が必要)

*3:変数の名前空間と関数の名前空間が同じ。