Clojureを紹介する記事はよくマクロの存在を最強の武器としてあげているが、実際にマクロはそう頻繁には使わない気がする。個人的にマクロを作る目印になるのはとあるウィキの記事に書いてある基準。

C++を主にみたデザインパターンの考え方の批判で指摘されるのは、ああやってパターンを繰り返し適応するのはまだコード化できてないなんらかの抽象化があることを示している。

コードに規則性や繰り返しが現れるのは、今使っている抽象化が十分ではない印。例えばマクロに任せるべきコード展開を手動でやっている。

Paul Graham: Revenge of the nerds

基礎的なことをやるマクロほどいろいろな使い方に対応しないといけないと同時に、合理的なデフォルトと使いやすさも保ちたい。逆にみると、その諸々の分岐や設定の対応をマクロを書くタイミングで固定費用として一回作っておくことでその後の開発効率がよくなる。

クエリを簡単かつきれいに書きたい

登場人物

  • next.jdbc
  • honeysql

要件

クエリを実行してデータベースからの返り値を処理して返すだけ。ただ考えてみればそこに含まれる隠れた要件も多い。クエリを実行する時は next.jdbc/execute! に渡せる設定マップはどうする?トランザクションを張りたい時はどうやって実現する?よくある種類のクエリの結果の事前処理をどうする?例えば UPDATEの返り行数が0だったらエラーにするとか。クエリの時間(honeysqlの format から execute! が返ってくるまで)を測ってログに出したいし。

可変個引数のマクロで可変個引数の関数を作る

クエリ関数を定義したいときにすでにわかっているものとしてはその名称、引数、クエリ本文と場合によって必要な execute! の設定マップ。設定が特にない場合はnext.jdbcのデフォルトを使うのか、そのアプリを作るに適した設定マップを渡す。つまりマクロは(中身は略すが)こう見えてくる。

(defmacro defquery
  ([name args query] ,,,)
  ([name args opts query] ,,,))

opts はどこで渡せるようにするのか悩んだが、結局 defn:pre:post を渡すマップと同様 args の次がいいと思った。

そして作られる関数に関してはnext.jdbcの関数と同様に第一引数にトランザクションなどデータソースを受け付けるようにしたい。なければアプリのデフォルトを使いたい。となると…

(defmacro defquery
   ([name args query]
    `(defquery ~name ~args ~query {}))
   ([name args opts query] 
    `(defn ~name
       (~args
         (~name db/default-datasource ~@args))
       ([datasource# ~@args]
         ,,,))))

この段階で頭がちょっとくるくるし始めた。でも後は毎回手動で書いていたクエリの実行(honeysqlの format の後にnext.jdbcの execute!呼んで以下略)を一箇所に書き下ろすだけで使うほとんどのクエリ定義はクエリの本文のマップだけで済むようになってすごくすっきりした。

クオートをネストしてみた

定義時にわかる情報で決められる分岐などはできるだけマクロの展開時に終わらせたい。その分実行も速くなるし、macroexpand で中身を確認する時に展開のバグも気づきやすい。

ただ展開時に分岐を済ませようとすると、高確率でクオート(`~)をネストするはめになる。そしてそこでシンボルの問題にぶつかる。Clojureのマクロを書く時は、バインディングを間違っても奪わないように、自動生成のシンボル(#がケツに付くやつ)を使う。ただその自動シンボル(autogensym)は一つのクオートに限定されて、一回 ~ でクオートを解除したら次は変わってしまう。こういうこと:

(defmacro my-macro
  [& args]
  (let [report-option# (last args)
        code-to-run# (butlast args)]
    `(try
        ~@code-to-run#
        (catch Exception e#
          ~(case report-option#
            :report `(println e#)
            :rethrow `(throw e#)
            nil)))))

こうしたマクロを展開してみると俺は期待してなかった結果になる:

(try
  (/ 1 0)
  (catch Exception e__1280__auto__
    (println e__1278__auto__)))

分岐は正しく消えてはいるが、なぜか生成された e# はそれぞれ違うシンボルになってしまっている。そして呼び出すとむろん”Unable to resolve symbol”のエラーになる。

残念ながらネストしたクオートを越えて自動生成のシンボルを使う方法は俺には知らない。Clojuriansで聞いてもやっぱ手動事前に gensym するしかない模様。

(defmacro my-macro
  [& args]
  (let [last-item# (last args)
        code-to-run# (butlast args)
        error-sym (gensym "e")]
    `(try
        ~@code-to-run#
        (catch Exception ~error-sym
          ~(case last-item#
            :report `(println ~error-sym)
            :rethrow `(throw ~error-sym)
            nil)))))

このように事前に letgensym して置くと、ちゃんと予想通りの動きにはなるが、クオートの中で ~error-sym が一体何を意味するか考えるだけで頭が痛くなる系。そして ~~error-symのように2重に unquote するとどうなるだろうと想像してみたが、クオートセプションが怖くて独楽を回して起きた。