minf/maxf

一つ前の記事では、以下のようなmaxf及びminfというマクロを定義して使っていた。

;;; 定義
(defmacro maxf (a &rest as) `(setf ,a (max ,@as)))
(defmacro minf (a &rest as) `(setf ,a (min ,@as)))

;;; 使用例
;; 「aの値が20よりも小さい場合は、aの値を20に設定する」
(defvar a 10)

;; 条件分岐を使った場合  ※ 比較したい値が3個以上になると複雑になる
(if (< a 20)
  (setf a 20))

;; setfとmaxを明示的に組み合わせた場合
(setf a (max a 20))

;; maxfを使った場合
(maxf a 20)


上ではminfなどの定義にdefmacroを直接使っているが、define-modify-macroという、より目的に沿ったマクロもあるらしい。

;; 上の定義と機能的に等価
(define-modify-macro maxf (&rest as) max)
(define-modify-macro mixf (&rest as) min)

汎変数関連のマクロはあまり使わないから忘れていた。
これくらいの定義なら、defmacroを用いた方が直截的で良いような気もするが、一応覚えておこう。

第三引数にlambda式を使う場合

ちなみに、define-modify-macroの第三引数は、lambda式でも良いらしい*1ので、引数に対して少し複雑な操作を行いたい場合はdefine-modify-macroの方が便利な時もあるかもしれない。
以下一例:

;;;;;;;;;;;;;
;; [マクロの動作]
;; リストの二番目の要素にvalをプラスする
;; ただし、valが数値ではない場合は、何も行わない
;;
;; 以下はdefmacroを用いた間違った定義
;; valに副作用がある式を渡した場合、二重に評価されてしまう危険性がある
(defmacro second+-if-number (lst val)
  `(progn 
     (when (numberp ,val)
       (incf (second ,lst) ,val))
     ,lst))

;; 動作例
> (defparameter a '(0 0 0))
> (defparameter b 1)

> (second+-if-number a b)
--> (0 1 0)
> a
--> (0 1 0)

> (second+-if-number a 'symbol)
--> (0 1 0)

;; 副作用がある式を渡した場合は、おかしな結果となる
> (setf a '(0 0 0)  b 1)

> (second+-if-number a (incf b))
--> (0 3 0) ; 期待した値は、(0 2 0)

> b
--> 3       ; (incf b)がマクロ内部で二回評価されているため+2されている


;;;;;;;;;;;;;;;
;; define-modify-macroを用いた定義
;; こっちのメリットは二つ
;;  - 二重評価の心配がない
;;  - 変数のquote/unquoteを気にしなくて済む
(define-modify-macro second+-if-number (val)
  (lambda (lst val)
    (when (numberp val)
      (incf (second lst) val))
    lst))

;; 動作例
> (setf a '(0 0 0)  b 1)

> (second+-if-number a (incf b))
--> (0 2 0) ; 期待通り

> b
--> 2       ; こっちも

*1:「らしい」と書いたのは、仕様では、第三引数に渡せるのは「a symbol」であると説明されているため。lambda式を用いた例も載っていない。