HOME Common Lisp 書き込む

2. 変数捕捉と多重評価


2.1. はじめに

関数の場合は引数は必ず1回評価されるのに対し、 マクロを使うと引数の評価の回数を制御することが出来ます。 このことを充分に認識していないと、意図せず変数を多重評価してしまう危険があります。 また、マクロは展開形が呼び出された場所に貼り付けられるので、展開形の中で不用意な 変数名を使っていると貼り付けられた場所にあった変数名と衝突してしまう危険があります。 これを避けるために、マクロ展開形の中では一意であることが保障されている gensym によって 生成されたシンボル (以下 gensym-symbol) を変数名に使う必要があります。

2.2. マクロ foronlisp の定義

マクロ for を使って多重評価と変数捕捉について説明したいと思います。 On Lisp にはさまざまなテクニックが解説されていますが、ここでは gensym を使う方法だけを解説します。

変数をある数から決まったステップで増加させて、ある数に達したら処理を 終了するマクロがあると便利です。dotimes は 0 からしかはじめられませんし、 いちいち do を書くのは面倒です。そこで、マクロ for を定義するわけですが、 まず最初は変数が1ずつ増加するバージョンを作ります。

(defmacro for ((var start stop) &body body)
  `(do ((,var ,start (1+ ,var))) ((>= ,var ,stop))                 ; (1) wrong
    ,@body))
さて、定義 (1) では stop がループが繰り返すたびに評価されています。 for の引数として式が与えられることも充分考えられますので、次の場合には 不都合を生じます。
(for (i 1 (long-calculation-taking-10-seconds))     ;example (1-1)
     (do-something i))

(let ((j 10))                                       ;example (1-2)
  (for (i 1 (incf j))
       (do-something i j)))
example (1-1) ではループが回るたびに不必要に 10 秒かかってしまいますし、 example (1-2) ではループは終了しません。 これは (1) の仮引数 stop が複数回評価されたことによるものです。

さて、多重評価を避けるために次のように改良したとしましょう。

(defmacro for ((var start stop) &body body)
  `(do ((,var ,start (1+ ,var)) (limit ,stop)) ((>= ,var limit))          ; (2) wrong
    ,@body))
(2) では多重評価を避けるために ,stop を limit に代入しました。これで、 多重評価は避けられましたが、変数が衝突する危険があります。 次の場合、ループはいきなり終了してしまいます。
(for (limit 1 10)
     (do-something limit))
意図しない変数の補足を避けるためには、gensym-symbol を使うのが 簡単で、確実です。for の正しい定義は (3) の様になります。
(defmacro for ((var start stop) &body body)                            ; (3) correct
  (let ((gstop (gensym)))
    `(do ((,var ,start (1+ ,var)) (,gstop ,stop)) ((>= ,var ,gstop))
       ,@body)))
これですと、引数の多重評価も起こりませんし、変数の捕捉も起こりません。 定義 (3) の2行目は展開形に現れず、3,4行目のみが展開形に現れます。
(pme (for (i 2 10) (princ i) (terpri))) [ctrl-j]
(do ((i 2 (1+ i))
      (#:G6 10))
     ((>= i #:G6))
   (princ i)
   (terpri))
ちなみに、for の仮引数が、((var start stop) &body body) の形をしていて、 var, start, stop が括弧でくくられています。これは、構造化代入といって、 defmacro では任意の構造の仮引数をとることが出来ます。

2.3. for の改良

前のセクションの for の定義では引数 var のステップごとの増加は1に固定されていましたが、 ここではそれを改良して、任意のステップ幅がとれるようにしましょう。
(defmacro for ((var start stop &optional step) &body body)                     ; (4)
  (and step (not (numberp step)) (error "step should be a number in \'for\'"))
  (and step (zerop step) (error "step should not be zero"))
  (let ((gstop (gensym)))
    `(do ((,var ,start ,(if step `(+ ,var ,step) `(1+ ,var))) (,gstop ,stop))
	 (,(if (or (not step) (plusp step)) `(>= ,var ,gstop) `(<= ,var ,gstop)))
       ,@body)))
"(if" の前にコンマがついていることに注目してください。これによって、step の 有無、正負によって生成するコードを 制御することが出来ます。また、step が数でなかったり、0 だったりするとコンパイル時に エラーが生ずるようにしてあります。

2.4. with-gensyms を使う

上で説明したように、マクロを書くときは以下のことに留意する必要があります。
  1. マクロ展開形内に現れるシンボルには gensym-symbol を使う。
  2. 複数回の評価を避けたい引数は gensym-symbol にバインドし、 引数自体はマクロ定義内で使わない。
即ち、マクロを定義する時には多量の gensym-symbol が必要になります。 以下にベクトルの内積または、行列の積の各要素を計算するマクロを示します。 これは matrix.lisp から取ってきました。 _with-gensym は前回定義した with-gensyms と同じものです。 clisp には すでに with-gensym というマクロが組み込まれており、基本的には前回定義したものと同じものですが、少し 凝ったことをしていてうまく働かない場合があります。そこで、ここでは名前を _with-gensym にして 定義しなおしました。マクロ v-ip は小さいマクロでありながら、4つの gensym-symbol を使っています。 このようにマクロ定義では gensym-symbol を多量に使うため、マクロ with-gensyms は重宝します。
(defmacro v-ip (v1 v2)
  (_with-gensyms (k len1 len2  s)
   `(let ((,len1 ,(if (consp v1) `(array-dimension ,(car v1) ,(1- (position '_ v1))) `(length ,v1)))
          (,len2 ,(if (consp v2) `(array-dimension ,(car v2) ,(1- (position '_ v2))) `(length ,v2)))
          (,s    0))
      (or (= ,len1 ,len2) (error "length different"))
      (dotimes (,k ,len1 ,s)
	(incf ,s (* ,(v-ip-fn v1 k) ,(v-ip-fn v2 k)))))))

;;; make `(aref ....) used in v-ip
(defun v-ip-fn (v0 k)
  (if (consp v0)
      (let ((p (position '_ v0)))
	`(aref ,@(subseq v0 0 p) ,k ,@(subseq v0 (1+ p))))
    `(aref ,v0 ,k)))

2.5. 終わりに

今回は gensym を使った、変数の捕捉と多重評価の回避について 解説しました。次回は構文変換について解説します。

不明な点、不正確な点などがありましたら紫藤まで お知らせいただけたら幸いです。(shido_takafumi@ybb.ne.jp)