#:g1: マクロに付くコンパイラマクロの使い道

Posted 2018-01-15 18:11:35 GMT

コンパイラマクロは関数だけでなくマクロにも定義できます。
HyperSpecのdefine-compiler-macroの項目にも明記されているのですが、一体どういう所で使うのかと思っていました。

コンパイラマクロの使い方としては、基本的にセマンティクスが変わらないことが絶対条件で、あとは何らかの効率が良くなるようなコード変換をすることになるんだと思います。
マクロでこういうパターンはどういう場合かを考えてみましたが、TAOのforみたいな場合に効率的な処理に展開できそうなので、ちょっと試してみました。

なお、マクロにコンパイラマクロを定義して、マクロなのにfuncallが効くように見せ掛けるテクニックを目にしたことがありますが、これはセマンティクスが変わってしまうのでNGかなと思っています。

TAOのfor

TAOのマニュアルによると、forは、

  形式 : for var list form1 form2  ...
form1 form2 ... を var を使って順に実行する。 var は list の各要素に
逐次束縛されたものである。 form1 form2 ... は list の長さと同じ回数評価
される。 nil を返す。

とあって殆どdolistと同じ挙動なのですが、indexというSRFI-1のiotaのような関数と組み合せて使う場合、実際には数値リストが生成されないという説明があります。

(index 0 10)(0 1 2 3 4 5 6 7 8 9 10) 

(let ((ans nil)) (for i (index 0 10) (push i ans)) ans)(10 9 8 7 6 5 4 3 2 1 0)

恐らく実際のTAOもマクロ展開のパターンマッチで展開を変えているんだと思いますが、こういうケースのマクロ展開にコンパイラマクロが使えそうです。

とりあえず、ベースの関数とマクロを定義します。

(defun index (start end &optional (step 1))
  (if (plusp step)
      (loop :for i :from start :upto end :by step :collect i)
      (loop :for i :from start :downto end :by (- step) :collect i)))

(defmacro for (var list &body body) `(dolist (,var ,list nil) ,@body))

コンパイラマクロで中間リストの生成を無くす

そしてコンパイラマクロを定義します。

下記では、無駄にメソッドを使っていますが、こういう場合には嵌るかなと思って試してみました。
メソッドディスパッチでindexだけでなく、iotaにも対応してみています。

(define-compiler-macro for (&whole whole var list &body body)
  (for-cm-expander (car list) var list body whole))

(defgeneric for-cm-expander (fn var list body whole) (:method (fn var list body whole) (declare (ignore fn var list body)) whole))

(defmethod for-cm-expander ((fn (eql 'index)) var list body whole) (declare (ignore whole)) (destructuring-bind (index start end &optional (step 1 stepp)) list (declare (ignore index)) (if stepp `(if (plusp ,step) (loop :for ,var :from ,start :upto ,end :by ,step :do (progn ,@body)) (loop :for ,var :from ,start :downto ,end :by (- ,step) :do (progn ,@body))) `(loop :for ,var :from ,start :upto ,end :do (progn ,@body)))))

(defmethod for-cm-expander ((fn (eql 'srfi-1:iota)) var list body whole) (declare (ignore whole)) (destructuring-bind (iota count &optional (start 0) (step 1)) list (declare (ignore iota)) `(loop :for ,var :from ,start :repeat ,count :by ,step :do (progn ,@body))))

こういう定義にすると、コンパイル時にはこういう展開になります。

(compiler-macroexpand '(for i (index 10 20) (print i)))
==>
(loop :for i :from 10 :upto 20 :do (progn (print i))) 

(compiler-macroexpand '(for i (srfi-1:iota 10) (print i))) ==> (loop :for i :from 0 :repeat 10 :by 1 :do (progn (print i)))

決め打ちのパターンから外れれば、リストを回す通常の処理になります。

(compiler-macroexpand '(for i (cons -1 (index 0 10)) (print i)))
==>
(for i (cons -1 (index 0 10)) (print i)) 

むすび

define-compiler-macroでマクロにコンパイラマクロを定義できるようにした経緯からユースケースを知りたかったのですが、過去のメーリングリスト等からは探し出せませんでした。

何か他のユースケースがあれば是非とも知りたいです。


HTML generated by 3bmd in LispWorks 7.0.0

comments powered by Disqus