nptclのブログ

Common Lisp処理系nptの開発メモです。https://github.com/nptcl/npt

define-compiler-macroを使ってみる

Common Lispdefine-compiler-macroを使ってみましょう。

ただ使うだけなら、検索すればすでに何人かの方が このマクロの正しい使い方を書いていますので、 そちらを見ていただいた方が絶対に有益な情報が得られると思います。

本投稿は、誰も話題に出していないsetfの宣言について説明します。

まずは普通の使い方

define-compiler-macroとは、コンパイルの時だけ展開するマクロです。
ふつうの関数だろうがマクロだろうが、 define-compiler-macroを使えば挙動を変えられるってことになります。
ただしコンパイルの時だけですが。

コンパイル」とは次の2つの関数で行われる動作のことです。

  • compile関数
  • compile-file関数

それでは例文を。

(defmacro aaa ()
  :hello)

(define-compiler-macro aaa ()
  :abc)

defmacroのマクロaaaは、ただ:helloを返却するだけです。
それに対してdefine-compiler-macroの方は、:abcを返却しています。

実行結果を示します。

(format t "~A~%" (aaa))
  -> :HELLO

(format t "~A~%" (funcall
                   (compile nil '(lambda () (aaa)))))
  -> :ABC

はい、何も難しくないですね。

setfを宣言するとどうなるか?

これが厄介なんです。
何が厄介なのかは大切なので、ちゃんと説明して行きます。

まず規約ではdefine-compiler-macroの第一引数は、function nameとされています。
function nameとは、symbol(setf symbol)のことなので、 規約として次のような宣言が許されます。

(define-compiler-macro (setf name) (...)
  ...)

しかし、これ以上の記載が無く例文も存在しません。
つまりどう実装するかは処理系に任されることになります。
では実験してみるとどうなるかというと、 sbcl, clisp, cclでは全て同じ動作をしたため、 まずはその挙動を紹介します。

(define-compiler-macro (setf aaa) (value inst)
  `(setf (cdr ,inst) ,value))

(funcall
  (compile nil '(lambda ()
                  (let ((x (cons 10 20)))
                    (setf (aaa x) 30)
                    (format t "~A~%" x)))))
-> (10 . 30)

例文では(setf aaa)(setf cdr)と同じ挙動になるように定義しています。
こう見ると何も問題は無いように見えます。

しかしそもそもsetfの定義は、上記のような通常のマクロでは表せられないものであり、 普通はdefine-setf-expanderによって定義するものです。

もし(setf cdr)と同じ挙動をする通常のsetfマクロを 作りたいのであれば次のようになります。

(define-setf-expander aaa (inst)
   (let ((g (gensym)))
     (values nil nil `(,g) `(setf (cdr ,inst) ,g) `(cdr ,inst))))

よって、次のようにdefine-compiler-macroを 定義する処理系が存在する可能性が出てきます。

;; ★こういう処理系もあるかもね
(define-compiler-macro (setf aaa) (inst)
   (let ((g (gensym)))
     (values nil nil `(,g) `(setf (cdr ,inst) ,g) `(cdr ,inst))))

存在するかどうかの確認はしていませんけどね。

個人的にはこの例の方が自然のように思えます。
では、なぜこうしなかったのか。

それは各処理系の都合なので私がわかるはずがないのですが、 もともとdefine-compiler-macroの目的とは、 最適化や高速化のために導入されたものなのではないでしょうか。
コンパイル時にはsetf形式を発見したら、 単純に置き換えるような仕組みであった方が何かと便利なのでしょう。

とはいっても、現状のsbcl, clisp, cclで見られる方式を採用した場合には 極めて大きな問題が浮上します。
それは、get-setf-expansion関数が使えないということ。
もっと分かりやすく言うと、setfマクロじゃなければ使えないということです。

例を示します。

(define-compiler-macro (setf aaa) (value inst)
  `(setf (cdr ,inst) ,value))

(funcall
  (compile nil '(lambda ()
                  (let ((x (cons 10 20)))
                    (shiftf (aaa x) 30)
                    (format t "~A~%" x)))))
-> ERROR, ★shiftfでは使えない

面白いことに、psetfならうまく行きました。

(funcall
  (compile nil '(lambda ()
                  (let ((x (cons 10 20)))
                    (psetf (aaa x) 30)
                    (format t "~A~%" x)))))
-> (10 . 30)

処理系を作る上では面倒この上ないです。
setfが現れたら、コンパイルの時だけdefine-compiler-macroの情報を 引っ張ってこなければいけないのですから。

いい加減な結論を出すとこんな感じ。

  • define-compiler-macro(setf symbol)は定義しない方がいいかもね