8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Clojureマクロの書き方

Last updated at Posted at 2021-04-25

はじめに

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)
8
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?