紙箱

覚えたことをため込んでいく

マクロを便利に使う

この記事はもともとTumblrに書いていた自分のブログ記事を転載したものです。投稿日時も当時の投稿日時を再現してあります。

マクロは自分で作るもんなのか?

Clojureでばりばり書いている人からすると愚問で、「Yes」としか言いようがない質問だろうけども、前に、とある場所でClojureについてのLTをしたときに、「マクロとかagentとかは実際に使うもんなんですかね?」と質問を受けたことがあります。

『フレームワークで使うテクニックなんかはフレームワークを「使う側」が知る必要はない』みたいな話の延長なのか、あるいは、そもそも現場での使いどころがイメージできない、ってことなのか、つかみ損ねましたが、実例があると分かりやすいのかなあと思いまして、実際に自分はどういう風に使っているのか、ここに書いておこうと思いました。

文法をつくれ!

やっぱり、実際に使う一番多い用途はこれだとおもうんですよね。新しい文法を作る。

とはいえ、ほとんどのClojureのテキストブックには、マクロに対するClojureの立場みたいなもんが書いてるはずです。つまり「必要ないならマクロを使うな!」と。

同じ書き方で関数でできることを、わざとマクロでやるのはいいかっこしいなんですよ。関数で出来ないからマクロでやる、であるべき、ということでしょう。

(まあ、マクロはデバッグもしにくいですし)

でも、こう書きたいけど関数ではできない、というケースは、思ったよりたくさんあります。関数で書いてみて、関数ではできないとわかったら、マクロでやればいいのです。

誰でも「ああそういうケースありそう」と思える実例として、次のようなケースを考えてみましょう。

値がnilでない時のみ、その値をマップに入れる

私が実際に使っている例として、ある値がnilでないなら、その値をマップに格納したい、という用途があります。

引数がnilだったらマップには入れたくない、とかいろいろ状況は考えられますが、単純な例として「マップ同士の移し替え」を考えてみましょう。

とりあえず、以下のようなデータがあるとします。便宜上Clojureのマップとして表現します。各キーは、存在したりしなかったりします。

{:book_name "本の名前"
 :isbn_code "ISBNコード"
 :publish_date "発行日"
 :publisher_name "発行会社"}

これを、次のようなマップに移し替えます。

{:type :book
 :name "本の名前"
 :isbn "ISBNコード"
 :published "発行日"
 :publisher "発行会社"}

:typeという独自キーを持っているのと、それぞれ微妙にキー名が異なるわけです。もしもとのマップに :book_name しか入っていなければ、次のようなマップを得たい。

{:type :book
 :name "本の名前"}

変換後も、元のマップにないキーは存在しないわけです。単純な詰め替え処理を書くと、次のようなマップが得られてしまうはずです。

{:type :book
 :name "本の名前"
 :isbn nil
 :published nil
 :publisher nil}

assocするとキーは生成されてしまうからです。すぐ使い捨てるデータならこれでもいいのですが、nilだったらキー自体が無くなって欲しいということもあるでしょう。

普通に関数で書くと、以下のような冗長なコードになります。もとのマップはoriginalというシンボルでアクセスできるものとします。

(cond-> {:type :book}
  (:book_name original) (assoc :name (:book_name original)
  (:isbn_code original) (assoc :isbn (:isbn_code original)
  (:publish_date original) (assoc :published (:publish_date original)
  (:publisher_name original) (assoc :publisher (:publisher_name original))

cond-> 命令は、左の式が真だったら、右の式を実行します。右の式の第1引数には、直前の式の結果が渡されます。初期値は :type のみをキーにもつマップで、最初の判定が真だったらこれが最初の式の第1引数に渡り、次も真だったら最初の式の結果が2番目の式の第1引数に渡ります。すべて真だったら、4回 assoc が実行されるわけです。

同じコードが4回並んでいることがわかります。いかにもめんどくさい。4つならいいけど、フィールドが20個とかあるとしんどい。

で、もやもやと「こういうふうに書きたいなあ」と思ったとします。

(assoc-> {:type :book}
    :name (:book_name original)
    :isbn  (:isbn_code original)
    :published (:publish_date original)
    :publisher (:publisher_name original))

右側の式が真だったら、右側の式の結果を、左側の値をキーにしてassocすることを繰り返すわけです。

「こうかけたらいいな」と思ったのなら、そう書けるようにすればいいわけです。元のコードがあり、「こう書きたい」というコードがあるなら、元のコードを、「こう書きたい」というコードに変換すればいいのです。コンパイル前にコードを書き換える機能、それがマクロです。

復習: マクロとはなにか

前述したようにマクロとは、コンパイル前に、ある式を別の式に変換する機構です。コンパイラは、変換後のコードをコンパイルします。

マクロとはClojureの関数で、引数は、Clojureのコードそのものです。また、結果としてClojureのコードを返します。ここで役に立つのが、Clojure(というかすべてのLISP)の特徴である、「プログラムコード自体がClojureのデータである」という性質です。

マクロには、引数としてClojureのコードそのものが渡ってきますが、これはソースという文字列の塊ではなく、「シンボルのリスト」として渡ってきます。たとえば、

(when (> age 19) (println "OK"))

whenはマクロです。whenマクロには、第1引数として(> age 19)というリストが、第2引数として、(println "OK")というリストが渡されます。
それぞれはリストですので、Clojureのあらゆるリスト操作関数が使えます。第1引数に対して first を使えば、「>」というシンボルを得られます。secondなら「age」が得られます。

引数にリストとして渡ってきたコードを、マクロ内で再構成し、新しいコードを表すリストを返せば、コンパイラはそれをコンパイルするのです。

つまり、前述の例であれば、「こうかければいいな」とモヤモヤ考えていたassoc->というマクロを書き、マクロ内で、実際に動作する、めんどくさいcond->を使った式に書き換えて返せばいいわけです。

別の関数を使って書けることであれば、全部マクロで書けます。その「書けることを分かっている」式に変換すればいいのですから。

Clojureには、マクロを書きやすくするために、マクロテンプレートとして使える「構文クオート」という仕組みがあります。「`」(バッククオート)で式を始めれば、それ以降はマクロテンプレートになります。マクロテンプレートはマクロが返す値であり、テンプレート内には、「~」あるいは「~@」を使って、マクロ外から受け取った値を埋め込めます。

assoc->マクロを作る

構文クオートを使うと、assoc->マクロは次のように書けます。

(追記: convert-clases関数は、あとから分配束縛を使えばもっと簡単に書けることに気がついたので、最初にポストしたあとに今のものに書き換えました)

(defn- convert-clauses
  [[k v & clauses :as data]]
  (when (seq data)
    (concat [v (list 'clojure.core/assoc k v)] (convert-clauses clauses))))

(defmacro assoc->
  [rec & clauses]
  (let [converted (convert-clauses clauses)]
  `(cond-> ~rec
    ~@converted)))

assoc->マクロの第2引数以降のすべての式は、clausesに入ります。convert-clausesは、clausesリストを引数に、

[(:book_name o) (clojure.core/assoc :key1 (:book_name o)
 (:isbn_code o) (clojure.core/assoc :key2 (:isbn_code o)]

というベクタを作り出します。convert-clausesの中で、Clojureソースコード自体を分配束縛(destructuring)でk, v, clausesというシンボルに分解したり、concatなどを使って操作していることが分かると思います。

assoc->マクロは~@を使って、このベクタを展開してテンプレート内に貼り付けます。これで晴れて、

(assoc-> {}
  :key1 (:book_name o)
  :key2 (:isbn_code o))

は、

(cond-> {}
  (:book_name o) (clojure.core/assoc :key1 (:book_name o)
  (:isbn_code o) (clojure.core/assoc :key2 (:isbn_code o))

といったcond->式に変換されます。

まとめ

Clojureのマクロは、Clojureのリストを引数に受け取って、Clojureの関数でコードを操作することができる仕組みです。

文字列を操作するのであれば、パース処理などが必要で大変ですが、Clojureであれば、「シンボルのリスト」をClojureで操作するだけで、コードを変換することが出来ます。

もし関数で書いてもうまく理想の書き方ができないなら、マクロでやってみればいいと思います。