はじめに
ClojureはJavaなど他の言語にはないユニークな機能が色々と備わっています。そのなかでも一際特徴的に思えるのがマクロです。マクロは強力な機能に思える反面、Clojure初学者にとっては少々難しい機能に思うので、初歩的な内容を整理してみました。かくいう私もClojure初学者です。
はじめの一歩
簡単なサンプル
マクロを新しく定義するには defmacro
を使います。次のマクロは引数で渡されたフォームの評価前と後にコンソール出力を行います。
(defmacro with-log [form]
(list 'do
(list 'println "Start")
form
(list 'println "End")))
マクロの使い方は関数と同じです。
(with-log (+ 1 1))
; eval (current-form): (with-log (+ 1 1))
; (out) Start
; (out) End
macroexpand
を使うとマクロがどのように展開されるか確認できます。
(macroexpand '(with-log (+ 1 1)))
; (do (println "Start") (+ 1 1) (println "End"))
マクロを使う利点
Clojureはマクロを使うことでClojure自身をカスタマイズできるようになっています。マクロを使うことで関数だと冗長な処理や複雑な処理を簡潔かつ直感的なコードにすることができます。
例えば clojure.core
に定義されている ->
マクロは入れ子になってしまう関数の呼び出しを簡潔な表現にしてくれます。Threading Macroと言うらしいです。
;; 直感的ではない処理が...
(.plusMonths (.plusDays (. LocalDate of 2020 1 1) 30) 5)
;; マクロを使うと直感的に表現できる
(-> (. LocalDate of 2020 1 1)
(.plusDays 30)
(.plusMonths 5))
clojure.core
に定義されている when
もマクロです。when
は1つ目の引数を評価しtrueだった場合、2つ目の引数以降を評価してくれます。
(when true (println "Hello"))
; Hello
; nil
(when true (println "Hello") (println "world"))
; Hello
; world
; nil
if
と似ていますが、 if
は2つ目の引数をelse句として扱います。
(if false (println "Hello") (println "world"))
; world
; nil
when
は関数を定義しても同じ機能を実装できるのでは?と思うかと思います。それはもちろんそうですが、幾分冗長な記述になってしまうデメリットがあります。というのも、引数のフォームをラムダ関数にする必要があるからです。
(defn when-fn [expr form]
; (if expr form))
; #'user/when-fn
(when-fn true #(println "Hello"))
; Hello
; nil
マクロを使用すると、マクロ展開時にClojureのコードが生成される性質のためラムダ関数にする必要がないです。 when
マクロは次のようにClojureソースコードを展開します。
(macroexpand '(when true (println "Hello")))
; (if true (do (println "Hello")))
マクロの定義
マクロを使うことは関数と同じですが、定義する方法は少し複雑です。というのもマクロはClojureのコードを生成するので、マクロ自体の構文と生成するコードを分けて考えていくことになるからです。
クオート
生成するコードは quote
を使って作ります。'
リーダーマクロを使用しても同じことができます。(リーダーマクロはここで説明しているマクロとは少し違う種類のマクロになります。)quote
は与えられたフォームを評価せずにそのまま出力します。
(quote (println "Hello"))
; (println "Hello")
'(println "Hello")
; (println "Hello")
quote
を他の関数と組み合わせて使えば、様々なコードを生成できます。例えば when
の定義では cons
を使ってコードを組み立てています。Clojureコードを使ってClojureコードを生成するのは少し違和感があるかもれませんが、これがマクロがとても強力な理由の一つだと思います。
(source when)
; (defmacro when
; "Evaluates test. If logical true, evaluates body in an implicit do."
; {:added "1.0"}
; [test & body]
; (list 'if test (cons 'do body)))
シンタックスクオート
シンタックスクォートを使うとより直感的にマクロを定義できます。
この記事の最初で定義した with-log
をシンタックスクォートを使って書き直すと以下のようになります。まるでテンプレートエンジンを使っているかのようにマクロが定義できます。
(defmacro with-log [form]
`(do (println "Start") ~form (println "End")))
; #'user/with-log
(macroexpand '(with-log (println "Hello")))
; (do (clojure.core/println "Start") (println "Hello") (clojure.core/println "End"))
(with-log (println "Hello"))
; Start
; Hello
; End
; nil
シンタックスクォートを使うには バッククォート `
を使います。バッククォートで修飾されたリスト内は以下のルールに従って解釈される仕組みになっています。
- unquote ...
~
で修飾されている場合、式として評価され、値が展開される - unquote-splicing ...
~@
で修飾されている場合、式として評価され、リストの中身が展開される - 修飾がない場合は、完全修飾付きでそのままの値が出力される
言葉で説明されただけだとどうしても分かりづらいと思うので実際に動かしてみます。
unquote
unquoteは次のように動作します。
;; x を定義する
(def x 10)
; #'user/x
;; x を評価すると 10 になる
x
; 10
;; シンタックスクオート内でunquote修飾されたxは評価されるので 10 になる
`(~x)
; (10)
;; 修飾されないxは評価されないので完全修飾付きでそのまま出力される
`(x)
; (user/x)
unquote-splicing
unquote-splicingは次のように動作します。
;; xList を定義する
(def xList (list 1 2 3))
; #'user/xList
;; xList は List として評価されている
xList
; (1 2 3)
;; シンタックスクオート内でunquote修飾すると List として評価される
`(~xList)
; ((1 2 3))
;; シンタックスクオート内でunquote-splicing修飾すると List の中の値が展開される
`(~@xList)
; (1 2 3)
;; 修飾されないxListは評価されないので完全修飾付きでそのまま出力される
`(xList)
; (user/xList)