👌

clojureマクロにおける展開の流れ

2025/02/04に公開

clojureマクロにおける展開の流れ

本記事ではclojureにおけるマクロ展開の流れについて考えていきます。

疑問

clojureに限らず、関数型言語ではこのような書き方をすることがあるかと思います。

;; active?に応じて関数を返す
(defn f [active?]
  (if active? function-a function-b))

;; 関数の適用
((f active?) arg)

fという関数はfunction-afunction-bを返し、それをargに対して適用するという流れです。関数型っぽい書き方で良いのではないでしょうか。

では、これと同様のことをマクロで行おうとするとどうなるでしょうか。

;; active?に応じてマクロを返す
(defmacro f-macro [active?]
  (if active? 'when 'unless))

(def active? true)

((f-macro active?) true (println "something printed"))

先ほどのコードと同様に、正常に動作しそうな気がしますよね。
しかし、実行してみたところ、Can't take value of a macro: #'clojure.core/whenというエラーが出てしまいます。

マクロと関数が違う概念なのは知っているが、なぜここで挙動の違いが発生するのか?と疑問に思いますよね。自分は思いました。

上述の挙動の原因

簡単にいうと、マクロ展開は以下のルールを満たしている場合のみ行われるからです。

  • フォームの一つ目の要素がマクロに紐づいているシンボルである

挙動の深掘り

まず、正常に展開されるマクロ記述を見てみましょう。

(when true (println "true"))

これがなぜ正常に展開されるかといえば、フォームの一つ目の要素がマクロに紐づいているシンボルであるという条件を満たしているからです。
フォームの一つ目の要素であるwhenがマクロに紐づいているシンボルであるが故に、マクロ展開が行われるのです。

では、先ほどの例を見てみましょう。

(defmacro f-macro [active?]
  (if active? 'when 'unless))

(def active? true)

((f-macro active?) true (println "something printed"))

まず、フォーム全体に注目します。このフォームの一番目の要素は(f-macro active?)であるため、このフォームに対してはマクロ展開は行われません
次にそれぞれの要素に注目していきます。一つ目の要素は(f-macro active?)であり、これはフォームの一つ目の要素 (f-macro) がマクロに紐づくシンボルであるため、展開が行われます。
その結果、(f-macro active?)'whenに展開されます。
その他のtrue(println "something printed")はマクロ展開の条件を満たさないため、そのままとなり、マクロ展開が終了します。

結果としてマクロ展開が終了した時点でのフォームは以下のようになります。

(when true (println "something printed"))

フォームとしては問題なく見えますが、whenはマクロであるためこの時点ではすでに展開されていなければなりません。
このようにwhenが展開されないままマクロ展開が終了してしまうため、エラーとなるというわけです。

まとめ

マクロ展開のルールは関数と同様で、子の要素の評価を行ってから親の評価を行うという流れなのかと思っていましたが、そうではないということですね。
評価の順番や有無を操作できるというマクロの性質上、子のマクロ展開を行ってから親のマクロ展開を行うという流れは不適切な事象や高い実装難易度などがあったのかもしれません。

おまけ

とはいえ、親→子の順のマクロ展開はほんまかというところもあったので、追加で実験をしてみました。

(defmacro macro [& args]
  (println args)
  `(str ~@args))

(macro (macro "a") (macro "b") (macro "c"))

マクロ展開が子→親の順で行われるのであれば、(a)(b)(c)(a b c)のような順になるはずです。
逆に親→子の順で行われるのであれば、((macro a) (macro b) (macro c))(a)(b)(c)のような順になるはずです。

評価の結果は以下となり、マクロ展開が親→子の順で行われていることが確認できました。

; eval (root-form): (macro (macro "a") (macro "b") (macro "c"))
((macro a) (macro b) (macro c))
(a)
(b)
(c)
"abc"

参考までに関数の場合は子→親の順で行われていることが確認されます。

(defn f [& args]
  (println args)
  (apply str args))
(f (f "a") (f "b") (f "c"))

評価

; eval (root-form): (f (f "a") (f "b") (f "c"))
(a)
(b)
(c)
(a b c)
"abc"

参考

https://clojurians.slack.com/archives/C053AK3F9/p1737359917343319

Discussion