LoLのdefmacro!をelispに移植

util-macroを集める旅の途中で「defmacro!いいよ」という声を聞いたので、LoLのdefmacro!をelispに移植してみました。と言っても、mapcarの部分をmapcar*に変えてみたりなどと少しコードを変更するだけで済みました。大したことはしてないです。

コードは以下においてあります。
https://github.com/podhmo/el-util-macro/blob/master/defmacro!.el

defmacro!について

defmacro!では、with-gensymsの機能とonece-onlyの機能を、式中のシンボル名で利用することができます。

  • G!fooはgensyms相当のものに置き換わる(名前の衝突を防ぐ)
  • O!fooは評価を一度切りに抑える(式の中で利用する際にはG!fooを利用)

defmacro!の利用例(square)

変数の値を2倍にするsquareというマクロを定義してみます。squareをdefmacro!を使わず思いついたまま定義してみると以下のようになります。*1

defmacro!を使わず思いついたまま定義。

普通思いつくのは以下のような感じです。

(defmacro square (x)
  `(* ,x ,x))

数値を渡した際の出力結果を見ると、合っているように見えますが…

(square 2) ; => 4

副作用を伴う式を渡すとおかしな結果を返してしまいます。

(let1 x 10
  (square (incf x))) ; => 132 = 11 * 12

これは展開してみると良く分かります。

(defmacro mp (form)
  `(let ((print-level nil)
         (print-length nil))
     (cl-prettyprint (macroexpand ',form))))

(mp (square (incf x)))
;;(* (incf x) (incf x))

このようにincfが評価される部分が2回出てきてしまいます。副作用を伴う式を渡すことを考えると評価は1回切りにしてほしい部分があります。

defmacro!を利用。

このようなマクロを手軽に定義するのにdefmacro!は効果を発揮します。defmacro!を利用したsquareの定義は以下です。

(defmacro! square (O!x)
  `(* ,G!x  ,G!x))

こちらは副作用が伴っても期待通りの結果を返します。

(square 10) ; => 100
(let1 x 10 
  (square (incf x))) ; => 121 = 11 * 11

展開結果は以下のようになります。

(mp (square (incf x)))
;; (let ((x8426 (incf x)))
;;   (* x8426 x8426))

incfが行われるのは一度切りですね。

このように便利なdefmacro!ですが、シンボル名により変形結果が変わるという新しい仕様を考えると、これまでのマクロと相性が悪いというか、強力過ぎるという思いがあるので今のところは収集中の便利マクロのライブラリの中には含めていません。

*1:LoLを読んでいる人にはこんな説明簡単過ぎて意味がないのかもしれませんが