语法(++ a) 是(incf a) 的无用别名。但是假设您想要后增量的语义:检索旧值。在 Common Lisp 中,这是通过 prog1 完成的,如:(prog1 i (incf i))。 Common Lisp 不会受到不可靠或模棱两可的评估顺序的影响。前面的表达式表示i被求值,值被隐藏在某处,然后(incf i)被求值,然后返回隐藏的值。
制作一个完全防弹的pincf (post-incf) 并非易事。 (incf i) 有一个很好的属性,i 只被评估一次。我们希望(pincf i) 也拥有该属性。所以这个简单的宏不够用:
(defmacro pincf (place &optional (increment 1))
`(prog1 ,place (incf ,place ,increment))
要做到这一点,我们必须求助于 Lisp 的名为 get-setf-expansion 的“分配位置分析器”来获取允许我们的宏正确编译访问的材料:
(defmacro pincf (place-expression &optional (increment 1) &environment env)
(multiple-value-bind (temp-syms val-forms
store-vars store-form access-form)
(get-setf-expansion place-expression env)
(when (cdr store-vars)
(error "pincf: sorry, cannot increment multiple-value place. extend me!"))
`(multiple-value-bind (,@temp-syms) (values ,@val-forms)
(let ((,(car store-vars) ,access-form))
(prog1 ,(car store-vars)
(incf ,(car store-vars) ,increment)
,store-form)))))
使用 CLISP 进行一些测试。 (注意:依赖于 get-setf-expansion 的材料的扩展可能包含特定于实现的代码。这并不意味着我们的宏不可移植!)
8]> (macroexpand `(pincf simple))
(LET* ((#:VALUES-12672 (MULTIPLE-VALUE-LIST (VALUES))))
(LET ((#:NEW-12671 SIMPLE))
(PROG1 #:NEW-12671 (INCF #:NEW-12671 1) (SETQ SIMPLE #:NEW-12671)))) ;
T
[9]> (macroexpand `(pincf (fifth list)))
(LET*
((#:VALUES-12675 (MULTIPLE-VALUE-LIST (VALUES LIST)))
(#:G12673 (POP #:VALUES-12675)))
(LET ((#:G12674 (FIFTH #:G12673)))
(PROG1 #:G12674 (INCF #:G12674 1)
(SYSTEM::%RPLACA (CDDDDR #:G12673) #:G12674)))) ;
T
[10]> (macroexpand `(pincf (aref a 42)))
(LET*
((#:VALUES-12679 (MULTIPLE-VALUE-LIST (VALUES A 42)))
(#:G12676 (POP #:VALUES-12679)) (#:G12677 (POP #:VALUES-12679)))
(LET ((#:G12678 (AREF #:G12676 #:G12677)))
(PROG1 #:G12678 (INCF #:G12678 1)
(SYSTEM::STORE #:G12676 #:G12677 #:G12678)))) ;
T
现在这里是一个关键的测试用例。在这里,这个地方包含一个副作用:(aref a (incf i))。这必须只计算一次!
[11]> (macroexpand `(pincf (aref a (incf i))))
(LET*
((#:VALUES-12683 (MULTIPLE-VALUE-LIST (VALUES A (INCF I))))
(#:G12680 (POP #:VALUES-12683)) (#:G12681 (POP #:VALUES-12683)))
(LET ((#:G12682 (AREF #:G12680 #:G12681)))
(PROG1 #:G12682 (INCF #:G12682 1)
(SYSTEM::STORE #:G12680 #:G12681 #:G12682)))) ;
T
所以首先发生的是 A 和 (INCF I) 被评估,并成为临时变量 #:G12680 和 #:G12681。访问数组并在#:G12682 中捕获值。然后我们有我们的PROG1,它保留了返回值。该值递增,并通过 CLISP 的 system::store 函数存储回数组位置。请注意,此存储调用使用临时变量,而不是原始表达式 A 和 I。 (INCF I) 只出现一次。