do-array

数値計算に現れるパターンの一つに、ループを使って配列の各要素に何らかの演算を行うというものがある。頻出するパターンと来ればマクロの出番だ。Common Lisp のマクロの書き方について非常にわかりやすく書いてある "Macros: Defining Your Own" (Practical Common Lisp) を参考にして今回のマクロを書いてみた。

;; (defparameter *xs* (make-array 4 :initial-contents '(1.0 2.0 3.0 4.0)))
;; Example-1
(do-array (x *xs*)
  (print x))

これは次のようなコードに展開されるとする。

(do ((i 0 (1+ i)))
    ((= i (length *xs*)))
  (let ((x (aref *xs* i)))
    (print x)))

マクロを書くと大体こんな感じか。i はマクロで使う変数なので展開後のコードと干渉しないように (gensym) が必要になる。

(defmacro do-array ((var array) &body body)
  (let ((i (gensym)))
    `(do ((,i 0 (1+ ,i)))
         ((= ,i (length ,array)))
       (let ((,var (aref ,array ,i)))
         ,@body))))

`macroexpand-1` で Example-1 を展開してみる。

(DO ((#:G1517 0 (1+ #:G1517)))
    ((= #:G1517 (LENGTH *XS*)))
  (LET ((X (AREF *XS* #:G1517)))
    (PRINT X)))

良さそうだ。

もうちょっと改良。目的を考えると要素数の等しい複数の配列を扱えるようにしたい。こんな感じ。

;; Example-2
(do-array ((x y) (*xs* *ys*))
  (print (+ x y)))

展開されたときに次のようになってほしい。

(do ((i 0 (1+ i)))
    ((= i (length *xs*)))
  (let ((x (aref *xs* i))
        (y (aref *ys* i)))
    (print (+ x y))))

ここで *xs* と *ys* の要素数は同じとする。もし異なる場合、本来なら要素数の少ない方に合わせてループすべきだが今回は省略。

マクロはこんな感じになる。対応する変数 `vars` と配列 `arrays` の数が同じかどうか一応チェック。

(defmacro do-array ((vars arrays) &body body)
  (assert (= (length vars)  (length arrays)))
  (let ((i (gensym)))
    `(do ((,i 0 (1+ ,i)))
         ((= ,i (length ,(car arrays))))
       (let (,@(mapcar #'(lambda (v a)
                           `(,v (aref ,a ,i)))
                       vars arrays))
         ,@body))))

let による変数の束縛の部分がポイント。束縛する変数と配列要素へのアクセスをリストとしてまとめた後に、,@ を使って分解する。Example-2 を展開してみる。

(DO ((#:G1533 0 (1+ #:G1533)))
    ((= #:G1533 (LENGTH *XS*)))
  (LET ((X (AREF *XS* #:G1533)) (Y (AREF *YS* #:G1533)))
    (PRINT (+ X Y))))

良さそうだ。

このマクロを使えば、たとえば次のような3つの配列に対する処理も

(let ((acc 0.0))
  (do ((i 0 (1+ i)))
      ((= i (length *xs*)))
    (let ((x (aref *xs* i))
          (y (aref *ys* i))
          (z (aref *zs* i)))
      (let ((xyz (* x y z))
            (xx (* x x)))
        (incf acc (+ xyz xx)))))
  acc)

以下のように簡潔かつ明快に書くことができる。

(let ((acc 0.0))
  (do-array ((x y z) (*xs* *ys* *zs*))
    (let ((xyz (* x y z))
          (xx (* x x)))
      (incf acc (+ xyz xx))))
  acc)

マクロはややこしく、今回もかなり試行錯誤だったが、幸い SLIME のおかげでかなり効率よく書くことができた。