ClojureマクロOverview

Clojure マクロの仕組みは2stepにわけられる.

  1. マクロ展開
  2. コンパイル

Clojureはマクロに出会うと, マクロがまず展開され, その結果がプログラム中のマクロのあった位置に置き換えられる(マクロ展開時処理). その次に通常のコンパイルが動く(コンパイル時処理).

マクロのデメリットは関数値にできないこと

マクロは第一級関数ではないので, 他の関数の引数として渡すことは出来ない.

Clojureは関数型言語で関数を引数に渡すことは多々あるため, このマクロのデメリットの影響は大きい.

macroexpand: マクロ展開

Clojureのマクロは関数と同じように使うことができる. そのため, predefinedなmacroは実は気づかないでたくさんつかっていることがある.

フォームの先頭にクオート(‘)をつけて macroexpand を叩くと, マクロ展開されてコンパイルに評価される前のフォームが現れる.

macroexpandのサブ関数として macroexpand-1 がある. REPL上でのデバック利用する. macroexpandはマクロが展開できる限り再帰的にmacroexpand-1を呼び出している, という関係.


Emacs CIDERにはcider-macroexpand-1/cider-macroexpand-allという関数があり, フォームの末尾でこれを叩くと別バッファに展開されたフォームが表示される.


ClojureのThreading Macrosの中身を理解するときにmacroexpandは活用できる.

Clojure: リーダマクロ

変な記号のシンタックスで表現されてClojureヤバそうという雰囲気を醸し出すもの. コードを簡単に記述するためのルールなので, 覚えてしまえばもう友達. 種類はそこそこある.

Clojure: シーケンス制御系(スレッディングマクロ他)

手続きを記述するために書きやすくしたマクロ. これらは別ページで解説.

Clojureのメイン機能ではないものの, あらゆるシーンで大活躍かつよく見かけるのがスレッディングマクロ.

またはand/orを活用した制御のマクロ.

副作用の手続きを書くdotoマクロ.

Clojure: 副作用のあるシーケンス制御(do/doto)

Clojure マクロ定義Basics

ここではPredefinedなマクロではなくて, 自分でマクロを定義する知識をまとめる.

defmacro: マクロ定義/マクロ評価

マクロを定義するには, defmacro をつかう.

symbolの前に quote またはシングルクオート(‘)をつけるとdefmacro内で評価されない.

  • マクロ引数に対してはなにもしなくていい.
  • nilは何度評価してもnilなのでなにもつけない.

シンタックスクオート

リーダマクロとして シンタックスクオート (または構文クオート) というのがある.

  • (`): synax-quote, `はバッククオート(backquote)
  • (~): unquote, ~はチルダ.
  • (~@): unquote-splicing

シンタックスクオートはクオートと同じ役割だが, シンタックスクオートとされたリストの中でチルダに出会ったときの挙動が異なる.

  • unquote(~)に出会うとそれ以後のクオートがoffになる
    • =すなわち式として評価されて値が展開される.
  • unquote-splice(~@)に出会うとそれ以後のクオートがoffになり, さらにリストの中身が展開される.

unquote-spliceは(&)を伴う可変数引数として渡されたフォームを評価しようとしたらカッコが余分についているパターンがとても多いため, その改善ために~を改良して作られたもの.

ref. Clojure - The Reader

gensym

https://clojuredocs.org/clojure.core/gensym

新しい名前を持つ新しいsymbolを返す. xxxx みたいな4ケタの数字. stringのprefixがなければ G__ みたいなやつが自動でつく.

# がgensymの糖衣構文になっている. symbolのおしりにつけるところがポイント.

これが必要となるシーンは, letのようなmacroの中で一次変数をbindingするケース.

Writing Macros | Clojure for the Brave and True


letと同じ理屈でマクロのなかで無名関数を使うときの仮引数でもgensymは必要.

(defmacro compile-route
  "Compile a route in the form (method path & body) into a function."
  [method route bindings body]
  `(make-route
    ~method ~(prepare-route route)
    (fn [request#]
      (let-request [~bindings request#] ~@body))))

Clojure functions and gensym - Stack Overflow

マクロ利用の指針

マクロ利用の指針 from Programming Clojure

  1. マクロを書くな.
  2. それがパターンをカプセル化する唯一の方法ならば, マクロを書け.
  3. 同等の関数に比べて, 呼び出し側が楽になるならばマクロを書いても構わない.

繰り返し現れる 特殊形式 (ex. if) を マクロで共通化する.

デザインパターンとは, 多態的なインスタンス化のパターンに過ぎない. 同様にして, マクロとは共通部分を括りだす層であり, プログラミング言語そのものの記法などの, 既存の文法では括りだすことが難しいところを共通部分として切り出す.

たとえば, unless記法がない言語では if !hogeのように!を多用する. しかし何度も何度も!が使われるならば, もはやその記法には独自の定義を与えたほうが読みやすいだろう, そういうことだ.

ref: 📚Programming Clojure

評価順序の操作と副作用

よくJavaをマクロでWrapするとClojureで言われるがもう少し広範囲で副作用を包むという視点はおもしろい.

また, たんに書きやすくするならば関数でいいのでこれはマクロのメインの役割ではない, という視点も.

yuwki0131-blog: On Lispを読んだ.

マクロパターン

by Programming Clojure

だいたい2つの分類, 6つのパターンに分類することができる.

ref: 📚Programming Clojure

ちょくちょく実例へのリンクを書き足していきたい(urlつきで).

  • 特殊形式
    • 条件的な評価
    • varの定義
    • Javaの呼び出し
  • 呼び出し側の便利さ
    • 評価の遅延
    • 評価を包む
    • lambdaの省略

🎓var定義パターン(def/defn)

varの定義と表現されているやつ. 実際にはvarを内部で利用している組み込みマクロ.

  • def
  • defn
  • defmacro
  • defmethod

defやdefnで作成された変数や関数をnamesapceに束縛することは関数ではできないからこそ, マクロでしかできない. たとえば, namepsaceにある規則に従った関数をたくさん定義するなど.

Clojureを学び始めた時, Twitter APIの関数をマクロで定義するをみて, はじめなにをやっているのかわからなかったが, マクロでTwitter APIの関数を全部定義していた. Native APIの生成に役立ちそう.

🎓評価の遅延マクロパターン

Clojureはとくに遅延シーケンスを多用するためdelay and forceを上手く書く方法としてmacroをつかうというもの.

仮にこれをmacroでやらないとすると, Clojure: constantlyなどを使って関数を値と渡して使うときにapplyする方法がある.

これはマクロを使わなくても出来るが見やすくなるので趣味のTipsなきがした.

(defmacro prep-app [app]
  `(atom (delay ~app)))

🎓ラッパーマクロパターン

前処理, メイン, 後処理みたいな処理がベストプラクティスであり, いろんなところでこのパターンを適用したいときにつかう. よく, with というprefixがつく.


プログラミングClojureのp179. 評価を包むマクロ. with-out-str がラッパーマクロのよい例だよと書いてあった.

(defmacro with-out-str
  [& body]
  `(let [s# (new java.io.StringWriter)]
     (binding [*out* s#]
       ~@body
       (str s#))))

ref. ラッパー(Wrapper Pattern)

References

Clojure マクロTips

ref. 📝Clojure Tips

関数名を名前変更するには?

可読性なための名前だけ変更.

(defmacro init! [& args] `(update! ~@args))

これとは別に, 高速化を目的とした definline というシンタックスもある. こっちは関数に:inlineのメタデータがつく.

文字列を関数名にするには?

clojure.core/symbolをつかう. キーワードで受け取ったものをclojure.core/nameでいったん文字列にしてsymbolで変換してもいい.

~(symbol req-method)
~(symbol (name req-method))

Clojure Macro Topics

Macro Exception Can’t bind qualified name

defmacro内で例外を扱うためのeを宣言すると出てくるメッセージ.

CompilerException java.lang.RuntimeException: Can’t bind qualified name:

eではなく e# のような名前をつける. #はgensym.

Clojure Macro Examples

マクロを使った実例を収集する.

Twitter APIの関数をマクロで定義する

https://github.com/drone-rites/clojure-twitter/blob/9c739e211545fefb9825cf42b0f56c53fc8eb62a/src/twitter.clj#L39

def-twitter-methodというマクロを定義して, 関数定義の処理マクロで記述.

📝Clojure: Twitter Bot Development

Clojure マクロInsights

💡シングルクオートとバッククオート詳説

シングルクオートとバッククオートの2つのシンタックスがあり, どちらもマクロのTopicsのなかで登場するので混同しがち. 自分がはじめに躓いたところなのでもう少し丁寧に.

シングルクオートはquoteのリーダマクロ. バッククオートはsyntax quoteのリーダマクロ.

synax-quoteとquoteの違いは, 引数としてはいったときにnamespaceを含むかどうか. これによりnamespaceの名前の衝突を防ぐ.

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

また, チルダにであったときの挙動が異なる.

🤔applyとは non-macro版のunquote-splicing?

関数もマクロも呼びだし方は同じだが, unquote-splicingの使うシーンはapplyと似ている. どちらもClojure: 可変長引数関数を受け取る時. & を使うケース.

Clojure: applyとは関数適用であり, 無名関数とリストを受け取って関数適用する.

(apply f '(1 2 3))
;; => (f 1 2 3)

これはリストのカッコを外して中身に対して関数をあてるような処理であり, unquote-splicingの動作に似ている. non-macro版がapply, macro版が~@なのかもしれない. non-macro版でのカッコを外すシンタックスはあるのかな?

🤔マクロ名の慣習

これが適切なのかわからないけれども, 観測しているネーミング慣習.

元となる関数名hogeに変更を加えたものを良く見る.

  • hoge*
  • hoge+

💡マクロ関数にapplyは使えないのでマクロのマクロを書く

マクロ関数は📝第一級関数ではないため, 他の関数の引数にすることができなく, そのためapplyを使うことができない.

ライブラリで関数かと思ったらそれがマクロで提供されている時, さらにその関数を拡張したようとしたときにapplyが使えない.

この場合はマクロを生成するマクロをさらに自分で書くことにより解決する.

References

Clojureのマクロに関する日本語情報もあまり多くないのでClojure初学者赤ちゃん日本代表として情報をまとめていきたい.