CLOJURE for the BRAVE and TRUEでClojure学習 part7

はじめに

Clojure の学習備忘録。今回はChapter 8: Writing Macrosでマクロの書き方について学ぶ。
これでようやく1章の全Chapter完了である(^o^)

前章がマクロをメカニズムを紹介するのに対して、今回はマクロのより実践的な話。

専門用語をどう訳すが適切なのかわからず進めているので、誤訳あるかもしれませんが、あしからず。

目次

Macros Are Essential

マクロは魅力的なものであるが、コードをド派手にする難解なツールと考えるべきではない。 実際、マクロは、少数のコアな関数やスペシャルフォームから多くの組み込み機能を提供してくれるもの。 when を例にみてみよう。

(when boolean-expression
  expression-1
  expression-2
  expression-3
  ...
  expression-x)

他の多くの言語では、特殊なキーワードを使ってしか条件式を作成できず独自の条件演算子を作る方法もないので、 whenif のようなスペシャルフォームかと思うかもしれないが実はそうではない。 when はマクロである。

マクロ展開すれば、 whenif, do で実装されていることがわかる。

(macroexpand '(when boolean-expression
                expression-1
                expression-2
                expression-3))
; => (if boolean-expression
;       (do expression-1
;           expression-2
;           expression-3))

Anatomy of a Macro

マクロ定義は関数定義とよく似ており、マクロ名や、オプションの説明、引数、処理内容を記述する。 マクロはほぼ必ずリストを返すが、これはClojureが評価できるようにデータ構造を変換するのがマクロの機能であることからわかるだろう。 マクロ定義では、関数や、マクロ、スペシャルフォームが使用でき、関数やスペシャルフォームと同じようにマクロを呼び出すことができる。

もうお馴染みの infix マクロを例にみてみよう。

(defmacro infix
  "Use this macro when you pine for the notation of your childhood"
  [infixed]
  (list (second infixed) (first infixed) (last infixed)))

このマクロは中間記法で書かれたリストを正しい並びに再編成する。

(infix (1 + 1))
; => 2

関数とマクロが異なる点として、関数の場合引数は評価されてから関数内に渡されるのに対して、マクロの場合評価されずにマクロ内に渡されるという違いがある。 これを上記の例でみてみよう。 (1 + 1) を評価しようとすれば、エラーをうけとるであろう。しかし、マクロで呼び出してるので (1 + 1) は評価されずに infix に渡される。 そして、マクロは Clojureが評価できるように first, second, last を使ってリストを再編成している。 マクロ展開をつかえば、、次の通り再編成されていることがわかる。

(macroexpand '(infix (1 + 1)))
; => (+ 1 1)

また、関数のようにマクロ定義のときに分割代入(destructuring)を使うことができる。

(defmacro infix-2
  [[operand1 op operand2]]
  (list op operand1 operand2))

分割代入された引数は、渡されたシーケンス引数の順番に対応してバインドされる。 infix-2 でも引数にシーケンスを受け取り、はじめの値は operand1 という名前に、 2番めの値は、 op という名前に、3つ目の値は、 operand2 という名前にというように 順番に分割代入されていく

またマクロでは、arityも使うことができる。 and, or といった基本的な演算子も実はこれを利用したマクロになっている。 and マクロをみてみよう。

(defmacro and
  "Evaluates exprs one at a time, from left to right. If a form
  returns logical false (nil or false), and returns that value and
  doesn't evaluate any of the other expressions, otherwise it returns
  the value of the last expr. (and) returns true."
  {:added "1.0"}
  ([] true)
  ([x] x)
  ([x & next]
   `(let [and# ~x]
      (if and# (and ~@next) and#))))

これ理解するには、 `, ~@ といったシンボル(後で学習する)を理解しないといけないが、重要なのは3つのマクロ内容があるということである。 0アリティの場合常に true を返し、1アリティの場合は,引数それ自身を返し、nアリティの場合再帰的に自身を呼び出している。 関数と同じようにマクロにも再帰が利用でき、残引数(& next)が使える。

Building Lists for Evaluation

マクロを書くためには、Clojureが評価できるリストを構築しないといけないが、これには通常とは異なる考え方が必要になってくる。 そのひとつとして、クォート式をつかって最終的に構築されるリストが評価されないようにしないといけない。 もっと一般的にいえば, シンボル の違いに対してより深い注意が必要になってくる。

Distinguishing Symbols and Values

たとえば、式を引数に受け取り、その評価結果をコンソール出力させ、さらにその結果を返すマクロをつくりたいとしよう。 マクロに次のようなことをしてほしいわけだ。

(let [result expression]
  (println result)
  result)

評価のため list 関数を使って、そのままこれを次のようにマクロにするかもしれない。

(defmacro my-print-whoopsie
 [expression]
 (list let [result expression]
       (list println result)
       result))

だがこの試みは失敗し、 Can't take the value of a macro: #'clojure.core/let という例外を受け取る。 なぜだろうか?

理由は、letをシンボルのまま返してほしかったのに、 let の参照先を取得しようとしてるからである。 ほかにも、 result, println もシンボルではなくそれの参照先を取得しようとしている。 よって、次のように書けばうまくいく。

(defmacro my-print
  [expression]
  (list 'let ['result expression]
        (list 'println 'result)
        'result))

シンボルのまま扱いたい場合、シングルクォートを先頭につける。これでシンボルを評価させずそのまま扱うことができる。 そして評価させないようクォートをつかうことが、マクロを書くことの重要な考え方となる。 もう少し細かくみていこう。

Simple Quoting

マクロを書くためにはシンボルの評価をさけるためクォートを使用するが、ここでクォートについて簡単におさらいして、マクロ内でどのように使用されるのかを確認しよう。

まずはじめに、シンプルな関数をクォートなしで使うと次の通り。

(+ 1 2)
; => 3

これにクォートつけると、評価前のデータ構造そのまま取得できる

(quote (+ 1 2))
; => (+ 1 2)

このリスト内の + はシンボルである。もしこのシンボルを評価すると、加算関数が得られる

+
; => #function[clojure.core/+]

一方これをクォートすると、加算シンボルが得られる

(quote +)
; => +

バインドされていないシンボルを評価すると例外が発生する

sweating-to-the-oldies
; => Unable to resolve symbol: sweating-to-the-oldies in this context

一方クォートすると、たとえバインドされていなくても、シンボルそれ自身を返す。

(quote sweating-to-the-oldies)
; => sweating-to-the-oldies

シングルクォートは、 (quote x) のリーダーマクロである。

'(+ 1 2)
; => 3

'dr-jekyll-and-richard-simmons
; => dr-jekyll-and-richard-simmons

実際 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)))

注目すべきは、マクロ定義で、 if, do がクォートされていることである。 これによりシンボルをシンボルのままとして扱うことができる。 実際にマクロ展開してみてみよう。

(macroexpand '(when (the-cows-come :home)
                (call me :pappy)
                (slap me :silly)))
; => (if (the-cows-come :home) (do (call me :pappy) (slap me :silly)))

もうひとつ別の組み込みマクロ unless をみてみよう。

(defmacro unless
  "Invert 'if'"
  [test & branches]
  (conj (reverse branches) test 'if))

ここでも if はクォートされるので評価されず最終的に次のようなリストを返す。

(macroexpand '(unless (done-been slapped? me)
                      (slap me :silly)
                      (say "I reckon that'll learn me")))
; => (if (done-been slapped? me) (say "I reckon that'll learn me") (slap me :silly))

(conj '(1 2) 3 4) ; => (4 3 1 2) であることを忘れず。

マクロを書くならばこのようにクォートを利用することが多いが、syntax quoteというもっと強力なツールもある。

Syntax Quoting

Syntax Quote(以下準クォートと呼ぶ)は、通常のクォートと同様、データ構造を評価せずに返すが、大きくことなる2つの点がある。 ひとつめは、準クォートは名前空間を含んだフルパスのシンボルを返す。実際に比較してみよう。

名前空間を含めずにクォートをつかったときは、名前空間は含まない。

'+
; => +

名前空間を含めると、出力も名前空間が含まれる。

'clojure.core/+
; => clojure.core/+

準クォートをつかえば、必ずフルパスのシンボルになる。

`+
; => clojure.core/+

リストをクォートすれば、再帰的に働く。

'(+ 1 2)
; => (+ 1 2)

準クォートを使うと、全要素にその効果が働く

`(+ 1 2)

準クォートが名前空間を含むのは、名前衝突を回避するためである。

準クォートのもうひとつの異なる点というのは、チルダ ~ をつかってクォートを解除できることだ。 準クォートされたフォーム内でチルダが登場すると、評価しない、フルパスにするという機能が失われる。

`(+ 1 ~(inc 1))
; => (clojure.core/+ 1 2)

チルダのあとに (inc 1) があるので、クォートされるのではなく、評価される。 チルダをつけなかったらフルパスのシンボルにして返す。

`(+ 1 (inc 1))
; => (clojure.core/+ 1 (clojure.core/inc 1))

文字列補間を知っているならば、準クォート・解除も同じようなものと考えられる。 どちらも、構造化された中にいくつかの変数をくみこんだテンプレートを作成する。 Rubyならば、文字列結合をつかって、 "Churn your butter, Jebedian!" を作成できる。

name = "Jebedian"
"Churn your butter, " + name + "!"

これを文字列補間を使って書けば、次のようにかける

"Churn your butter, #{name}!"

文字列補間のほうが、見やすいだろう。 これと同じで準クォート・解除で書いたほうが見やすくなる。

(list '+ 1 (inc 1))
; => (+ 1 2)

`(+ 1 ~(inc 1))
; => (clojure.core/+ 1 2)

準クォートを使ったほうが簡潔であり、また最終的に生成されるフォームに近いので理解しやすいのだ。

Using Syntax Qutoing in a Macro

準クォートのメカニズムを理解できたので、 code-critic マクロについて見てこう。 このマクロを準クォートを使ってより簡潔なものに書き直す。

(defmacro code-critic
  "Phrases are courtesy Hermes Conrad from Futurama"
  [bad good]
  (list 'do
        (list 'println
              "Great squid of Madrid, this is bad code:"
              (list 'quote bad))
        (list 'println
              "Sweet goriilla of Manila, this is good code:"
              (list 'quote good))))

(code-critic (1 + 1) (+ 1 1))
; => Great squid of Madrid, this is bad code: (1 + 1)
; => Sweet goriilla of Manila, this is good code: (+ 1 1)    

これを準クォートをつかって書き直すと、非常に見やすくなる。

(defmacro code-critic
  [bad good]
  `(do (println "Great squid of Madrid, this is bad code:"
                (quote ~bad))
       (println "Sweet goriilla of Manila, this is good code:"
                (quote ~good))))

ここでは、 good, bad シンボル以外をすべてクォートしてる。 はじめのコードでは個々の要素ごとにクォートしたりリスト関数をつかって明示的にその要素として記述するのに対して、 準クォートをつかった場合、 do 式をクォートして評価したい2つのシンボルだけクォート解除するだけでよい。

以上をまとめると、マクロは任意の未評価のデータ構造を引数に受けとり、Clojureが評価できるデータ構造を返すものである。 マクロ定義するとき、関数と同様、引数の分割代入や、 let を使用することができる。さらに、アリティや再帰的にマクロを呼び出すこともできる。

マクロはリストを返す。リスト構築するためには、 list 関数または準クォートが使用できるが後者をつかったほうが 簡潔なコードになる。そして、もし複数のフォームを返したい場合は、 do で囲ってやればよい。

Refacoring a Macro and Unquote Splicing

前節で扱った code-critic にはまだ改良の余地がある。 println が2回呼ばれていることである。これをきれいにしていこう。 まずはじめに、この println リストを作成するヘルパー関数を作成する。

(defn criticize-code
  [criticism code]
  `(println ~criticism (quote ~code)))

(defmacro code-critic
  [bad good]
  `(do ~(criticize-code "Cursed bacteria of Liberia, this is bad code: " bad)
       ~(criticize-code "Sweet sacred boa of Western and Eastern Samoa, this is good code:" good)))

注目すべきは、ヘルパー関数の criticize-code の戻り値が、準クォートのリストということである。 これでマクロが適切なリストを返してくれるようになる。

まだ改良の余地はあり、同じ関数を複数回呼び出している。これを回避するために、 map をつかって改善してみる。

(defmacro code-critic
  [bad good]
  `(do ~(map #(apply criticize-code %)
        [["Great squid of Madrid, this is bad code:" bad]
         ["Sweet gorila of Manila, this is good code:" good]])))

良さそうに見えるので、実行してみよう。

(code-critic (1 + 1) (+ 1 1))
; => (NullPointerException) at user/eval6154

残念だがうまくいかない。なぜだろうか? 問題は、 map が返す値がリストであること、この場合、 println 式をリストで囲ったものを返すことにある。 println の呼び出し結果を得たいのだが、それをリストの中にいれて評価しようとしてしまうのである。

次のようなコードが実行されてしまっているのである。

(do 
  ((clojure.core/println "criticism" '(1 + 1))
   (clojure.core/println "criticism" '(+ 1 1))))

println はリスト内にあるので評価されてしまう。

(do 
  (nil
   (clojure.core/println "criticism" '(+ 1 1))))

最終的に次のようになってしまう。

(do 
  (nil nil))

こういう仕組みで例外が発生してしまった。 printlnnil に評価されて、最終的に (nil nil) になり、 NullPointerException が発生する。

どういうことかわかりにくが、要するに返す値がリストになってることが問題。 printlnを括弧で囲わないでほしいのに、囲われてるので評価されてアウトというわけだ。 *1 => (nil) => error という流れになってる。

clojure (macroexpand '(code-critic (1 + 1) (+ 1 1)))

この問題を解決してくれるのが、unquote splicing(以下スプライシングと呼ぶ)である。 スプライシングは、 ~@ を使う。通常のクォート解除は次の通りになる。

`(+ ~(list 1 2 3))
; => (clojure.core/+ (1 2 3))

一方、スプライシングを使用すると、次のようになる。

`(+ ~@(list 1 2 3))
; => (clojure.core/+ 1 2 3)

スプライシングを使用すると、シーケンスデータの括弧がなくなり、準クォート内に配置される。 これをさきほどのマクロに使用すると以下の通り。

(defmacro code-critic
  [good bad]
  `(do ~@(map #(apply criticize-code %)
              [["Sweet lion on Zion, this is bad code:" bad]
               ["Great cow of Moscow, this is good code:" good]])))
(code-critic (1 + 1) (+ 1 1))

Things to Watch Out For

ここでは、マクロ使用時につまずくポイントとその回避方法について説明する。

variable Capture

Variable Captureはマクロ利用側が知らないうちに既存のバインディングを上書いてしまう現象である。 次のコードは、マクロ内の let で混乱した結果を招くものである。

(def message "Good job!")
(defmacro with-mischief
  [& stuff-to-do]
  (concat (list 'let ['message "Oh, big deal!"])
           stuff-to-do))

(with-mischief
  (println "Here's how I feel about that thing you did:" message))
; => Here's how I feel about that thing you did: Oh, big deal!

println がシンボル message を参照するとき、 "Good Job!" がバインドされていると思うかもしれない。 しかし、 with-mischief マクロが message に新たなバインディングを作成してしまうので期待通りにはいかない。

準クォートを使うと、例外が発生する。

(def message "Good job!")
(defmacro with-mischief
  [& stuff-to-do]
  `(let [message "Oh, big deal!"]
        ~@stuff-to-do))

(with-mischief
  (println "Here's how I feel about that thing you did:" message))

この例外はありがたい。というのも、準クォートはマクロ内のVariable Captureの発生を防いでくれてるのからである。 もしマクロ内で let を使いたいならば、 gensym を使えばよい。 gensym は呼び出すたびにユニークなシンボルを生成してくれる。

(gensym)
; => G__6145

(gensym)
; => G__6148

(gensym) #+ENDSRC

G__6145G__6148

シンボルにプレフィックスをつけることもできる。

(gensym 'message)
; => message6151

(gensym 'message)
; => message6154

gensym をつかってマクロを書き直すと、次のとおりになる。

(def message "Good job!")
(defmacro with-mischief
  [& stuff-to-do]
  (let [macro-message (gensym 'message)]
    `(let [~macro-message "Oh, big deal!"]
        ~@stuff-to-do
        (println "I stil need to say:" ~macro-message))))

(with-mischief
  (println "Here's how I feel about that thing you did:" message))

gensym で新しいユニークなシンボルを生成して、それを macro-message にバインドする。 準クォート内の let では、 macro-message はクォート解除されて、gensymで生成されたシンボルとなる。 このシンボルは stuff-to-do とは異なるシンボルになるため、variable captureを回避できる。

こういったケースはよくあるので、 auto-gensym というもっと便利なものが使える

`(blarg blarg#)
; => blarg blarg__6168__auto__

`(let [name# "Larry Potter"] name#)
; => (clojure.core/let [name__6171__auto__ "Larry Potter"] name__6171__auto__)

順クォート内のシンボルにハッシュマーク(#)を付け加えることで、auto-gensymが作成される。 このように gensym やauto-gensymはマクロを書いてるときに陥るvariable captureを回避するときに必ず使用される。

Double Evaluation

variable captureの他にも double evaluation(二回評価) というトラップもマクロを書くときにある。 これは、マクロの引数のフォームが複数評価されてしまうものだ。

(defmacro report
  [to-try]
  `(if ~to-try
       (println (quote ~to-try) "was successful:" ~to-try)
       (println (quote ~to-try) "was not successful:" ~to-try)))

(report (do (Thread/sleep 1000) (+ 1 1)))

このマクロは、引数の真偽判定に応じて、成功・失敗どちらかの出力を行うものである。 (Thread/sleep 1000) は2回評価されてしまうので、2秒待たされてしまう。 1回目はifの直後、2回目は println が呼ばれたときである。 これが起きるのは、マクロ展開で (do (Thread/sleep 1000) (+ 1 1)) が複数現れてしまうからで、次のようなコードが実行されている。

(if (do (Thread/sleep 1000) (+ 1 1))
  (println '(do (Thread/sleep 1000) (+ 1 1))
           "was successful:"
           (do (Thread/sleep 1000) (+ 1 1)))

  (println '(do (Thread/sleep 1000) (+ 1 1))
           "was not successful:"
           (do (Thread/sleep 1000) (+ 1 1))))

この問題を回避するには、次のようになる。

(defmacro report
  [to-try]
  `(let [result# ~to-try]
      (println result#)
      (if result#
      (println (quote ~to-try) "was successful:" result#)
      (println (quote ~to-try) "was not successful:" result#))))

(report (do (Thread/sleep 1000) (+ 1 1)))

to-trylet 内に配置することで、1回だけ評価されるようにして、その結果をauto-gensymで生成されたシンボル(result#)にバインドする。 これで2回評価されるこはなくなるわけだ。

Macros All the Way Down

マクロ展開が評価の前に行われることで、ハマってしまう問題もある。 たとえば、 report マクロを 単独で複数回呼び出せば問題ないが、 doseq をつかって繰り返し呼び出すときうまくいかないことがある。

(report (= 1 1))
; => (= 1 1) was successful: true

(report (= 1 2))
; => (= 1 2) was successful: false

doseq をつかえば次の通り、期待通りにいかない。

(doseq [code ['(= 1 1) '(= 1 2)]]
  (report code))
; => code was successful: (= 1 1)
; => code was successful: (= 1 2) おかしい

reportマクロを単体で呼び出したときはうまく機能するが、 doseq で繰り返したときは機能していない。 この理由は、 doseq の各イテレーションごとのマクロ展開をみてみるとわかる。

(if
 code
 (clojure.core/println 'code "was successful:" code)
 (clojure.core/println 'code "was not successful:" code))

report マクロの各イテレートの code シンボルは評価されない。 report のマクロ展開時には、評価結果にアクセスできないのだ。

これを解決するためには、次のようなマクロを書けばよい。

(defmacro doseq-macro
  [macroname & args]
  `(do 
      ~@(map (fn [arg] (list macroname arg)) args)))

(doseq-macro report (= 1 1) (= 1 2))
; => (= 1 1) was successful: true
; => (= 1 2) was not successful: false

Brews fro the Brave and True

ここでは、 if-valid というバリデーション機能をもったマクロを書いていく。 自分でマクロを書くのにとってもよい訓練となるだろう。

Validation Functions

シンプルに考えたいので、名前とメールアドレスのバリデーションについて考える。 我々のお店では、次のような注文明細を受け取るようにしたい。

(def order-details
  {:name "Mitchard Blimmons"
   :email "mitchard.blimmonsgmai.com"})

このハッシュマップのメールアドレスは@がないので無効であるが、これをバリデーションでつかまえたいのである。 理想的には、次のようなコードが書きたい。

(validate order-details order-details-validations)
; => {:email ["Your email address doesn't look like an email address."]}

検証対象のデータ(order-details)と、バリデーション定義(order-detail-validations)を引数にとる validate 関数を呼び出せるようにしたいのである。 関数適用結果として、無効なフィールドをkeyに、無効の理由をベクタとしてvalueにもつハッシュマップを受け取りたい。

早速 order-details-validations から実装していこう。

(def order-details-validations
  {:name
    ["Please enter a name" not-empty]
   :email
    ["Please enter an email address" not-empty

     "Your email address doesn't look like an email address"
     #(or (empty? %) (re-seq #"@" %))]})

検証したいkeyに、エラーメッセージと検証関数のペアをベクタが紐付いたハッシュマップである。 たとえば、 :name キーは、ひとつの検証関数(not-empty)をもち、 検証結果がエラーならば、 Please enter a name というエラーメッセージを受け取る

次に validate 関数を実装しよう。 validate 関数は2つの関数で構成され、 ひとつは該当フィールドに検証関数を適用する関数で、 もうひとつは、 {:email ["Your email address doesn't look like an email address."]} のように検証結果のエラーメッセージをひとつにまとめる役割を持つ関数である。

次の error-message-for は前者を実装したものである。

(defn error-messages-for
  "Return a seq of error messsage"
  [to-validate message-validator-pairs]
  (map first (filter #(not ((second %) to-validate))
                    (partition 2 message-validator-pairs))))

第一引数の to-validate は検証したい値である。第二引数の message-validator-pairs は偶数個の要素をもつシーケンスデータである。 (paritition 2 message-validator-pairs) でペアに分割され、第一要素がエラーメッセージ、第二要素が関数となる(order-details-validations と同じ並びになる)。

error-message-for 関数は、エラーメッセージと検証関数のペアごとに、 to-validate を検証してtrueになった場合ものを絞り込む。 そして、 map first をつかってエラーメッセージをのみを返す。

(error-messages-for "" ["Please enter a name" not-empty])
; => ("Please enter a name")

では、つぎにエラーメッセージをハッシュマップにまとめる関数を実装して仕上げよう。

(defn validate
 [to-validate validations]
 (reduce (fn [errors validation]
           (let [[fieldname validation-check-groups] validation
                 value (get to-validate fieldname)
                 error-message (error-message-for value validation-check-groups)]
             (if (empty? error-message)
              errors
             (assoc errors fieldname error-message))))
         {}
         validations))

(validate order-details order-details-validations)
; => {:email ("Your email address doesn't look like an email address")}

if-valid

完成したバリデーション関数は、大抵次のようにつかうだろう。

(let [errors (validate order-details order-details-validations)]
  (if (empty? errors)
    (println :success)
    (println :failure errors)))

共通してつかわれるところは、

  1. レコードを検証して、その結果を errors にバインドする
  2. エラーがあるかどうかチェックする
  3. もしエラーがなければ、それに応じた処理が実行される。ここでは、 (println :success) になる。
  4. エラーがあれば、それに応じた処理が実行される。ここでは、 (println :failure errors) になる。

これを本番コードに導入してると、同じようなコードを繰り返し書いてることに気がついて、抽象化が必要だと気づくようになった。 具体的には、 validate 関数を適用して、結果をあるシンボルにバインドして、バインド結果からエラーがあるかどうかを確認する部分である。 抽象化のために、次のようなコードを書くかもしれない。

(defn if-valid
  [record validations success-code failure-code]
  (let [errors (validate record validations)]
    (if (empty? errors)
      success-code
      failure-code)))

これはうまく動かない。というのも、 success-code, failure-code が評価されてしまうからである。 しかし、評価のコントールできるマクロをつかえば、うまく動作させられる。次のようにマクロを使えるようにしよう。

(if-valid order-details order-details-validations errors
  (render :success)
  (render :faliure errors))

このマクロは、繰り返しの部分をうまく回避し、実現したいことを簡潔に表現してくれる。 早速実装してみよう。

(defmacro if-valid
  "Handle validation more concisely"
  [to-validate validations error-name & then-else]
  `(let [~error-name (validate ~to-validate ~validations)]
     (if (empty? ~error-name)
       ~@then-else)))

このマクロは、 to-validate, validations, errors-name, then-else の4つの引数をもつ。 errors-name を使うのは新しい戦略である。 then-else 内でvalidate関数の結果を使用したいので、errors-nameを用意して検証結果バインドさせている。 マクロ展開すると、次のようになる。

(macroexpand
  '(if-valid order-details order-details-validations my-error-name
       (println :success)
       (println :failure my-error-name)))

; (let*
;   [my-error-name (user/validate order-details order-details-validations)]
;   (if (clojure.core/empty? my-error-name)
;     (println :success)
;     (println :failure my-error-name)))

準クォートで let/validate/if を使ったフォームを抽象化して、 さらにスプライシングif 分岐で使って、残引数の then-else を適切に評価できるようにしている。

Summary

本章では、マクロの書き方について学習してきた。マクロは関数定義とよく似ており、引数、docstring、処理内容がある。 また分割代入、残引数、再帰も使える。マクロはリストを返す。単純なマクロならば、 list, seq を使うときもあるが、 多くの場合、 ` 準クォートで書くことになるだろう。

マクロを書くときには、シンボルと値の違いを意識することが重要である。 マクロ展開されたあとに評価されるので、評価結果にマクロ内ではアクセスできない。 重複評価やvariable captureもトラップになるが、 let 式とgensymsを賢く使用すれば回避できる。

マクロはより自由にプログラムできる楽しいツールである。プログラマが評価をコントロールできるため、 他の言語では不可能な表現を可能にしてくれる。Clojureの学習のなかで、マクロは危険、つかうべきでないといった意見を聞くこともあるだろう。 しかし、少なくともはじめは、これらの意見に耳を傾けてはいけない。率先して使って楽しみなさい。 使ってみないとマクロの最適な箇所はわからないからである。そのうちに反対派の意見もわかってくるようになるからだ。

*1:println …