TL;DR

マクロを展開する過程で、見かけ上同じ識別子が生まれても(名前が衝突しても)、意図しない問題が起きないことが保証されたマクロ。ハイジェニックマクロとも呼ぶ。

なぜ必要か

健全なマクロの機能を持たない言語では、マクロ展開の中で作った変数の束縛が、すでにある変数の束縛を隠してしまうことがある。これを不健全なマクロという。

例

C のマクロは健全ではない。次のコードでは、incr の中で宣言した a が呼び出し側の a を隠すので、incr(a) は外側の a を増やさない。

#define incr(X) {int a=0; ++X;}
int main (void) {
  int a=0, b=0;
  incr(a);
  incr(b);
  printf("a=%d, b=%d\n", a, b);
  return 0;
}
 
/* 展開後 */
int main (void) {
  int a=0, b=0;
  {int a=0; ++a;}
  {int a=0; ++b;}               /* ここで a が再宣言されている */
  printf("a=%d, b=%d\n", a, b); /* a=0, b=1 */
  return 0;
}

Clojure では、構文クォートの中で名前空間の修飾のない名前に # を付けると、アンダースコアとユニークな ID を付けた名前のシンボル(auto-gensym)が自動で作られる。これで名前の衝突を避けられる。

(defmacro unless [test then else]
  `(let [x# ~test]
     (if (not x#)
       ~then
       ~else)))
 
(let [x 10]
  (unless false
    (printf "x: %s%n" x) ;; x: 10
    (println "TRUE")))
 
(use 'clojure.walk)
;; 展開処理
(macroexpand-all '(let [x 10]
  (unless false
    (printf "x: %s%n" x)
    (println "TRUE"))))
 
;; 展開結果
(let* [x 10]
  (let* [x__2__auto__ false]
    (if (clojure.core/not x__2__auto__)
      (printf "x: %s%n" x)
      (println "TRUE"))))

Effective Rust 項目28 によると、Rust では macro_rules! を使う宣言的マクロが健全なマクロである。

自分の理解

Common Lisp でも同じ問題が起きることがあり、そのときは gensym を使って回避した記憶がある。Clojure については、ちょうど探していた内容の記事があった。

関連

出典