在 Common Lisp 中编写 ++ 宏
-
09-06-2019 - |
题
由于语义原因,我一直在尝试编写一个 Lisp 宏,它可以执行与其他编程语言中的 ++ 等效的功能。我尝试用几种不同的方式来做到这一点,但它们似乎都不起作用,并且所有方式都被解释器接受,所以我不知道我的语法是否正确。我对如何定义它的想法是
(defmacro ++ (variable)
(incf variable))
但这在尝试使用它时给了我一个简单类型错误。怎样才能让它发挥作用呢?
解决方案
请记住,宏返回要计算的表达式。为此,您必须反引号:
(defmacro ++ (variable)
`(incf ,variable))
其他提示
前面的两个答案都有效,但是它们给了你一个你称之为的宏
(++ varname)
而不是 varname++ 或 ++varname,我怀疑你想要。我不知道你是否真的能得到前者,但对于后者,你可以做一个读取宏。由于它是两个字符,因此调度宏可能是最好的。未经测试,因为我没有方便的运行 lisp,但类似:
(defun plusplus-reader (stream subchar arg)
(declare (ignore subchar arg))
(list 'incf (read stream t nil t)))
(set-dispatch-macro-character #\+ #\+ #'plusplus-reader)
实际上应该使 ++var 读 作为(incf var)。
我强烈建议不要为 incf 创建别名。这会降低其他阅读你代码的人的可读性,他们必须问自己“这是什么?和incf有什么不同?”
如果您想要一个简单的后增量,请尝试以下操作:
(defmacro post-inc (number &optional (delta 1))
"Returns the current value of number, and afterwards increases it by delta (default 1)."
(let ((value (gensym)))
`(let ((,value ,number))
(incf ,number ,delta)
,value)))
语法 (++ a)
是一个无用的别名 (incf a)
. 。但是假设您想要后增量的语义:检索旧值。在 Common Lisp 中,这是通过以下方式完成的 prog1
, ,如: (prog1 i (incf i))
. 。Common Lisp 不会遭受不可靠或不明确的评估顺序的困扰。前面的表达式意味着 i
被评估,并且该值被隐藏在某个地方,然后 (incf i)
被评估,然后返回隐藏的值。
制作完全防弹的 pincf
(邮政-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)
仅出现一次。
从语义上讲,C++ 等语言中的前缀运算符 ++ 和 -- 与 Common Lisp 中的 incf/decf 等效。如果您意识到这一点,并且像您的(不正确的)宏一样,实际上正在寻找语法更改,那么您已经了解了如何使用像`(incf,x)这样的反引号来做到这一点。甚至还向您展示了如何让读者破解此问题以获得更接近非 Lisp 语法的内容。但这就是问题所在,因为这些都不是一个好主意。一般来说,使用非惯用的编码来使一种语言更类似于另一种语言并不是一个好主意。
但是,如果实际上正在寻找语义,您已经获得了如上所述的前缀版本,但后缀版本在语法上并不容易匹配。你可以通过足够的读者技巧来做到这一点,但它不会很漂亮。
如果这就是您正在寻找的,我建议 a) 坚持使用 incf/decf 名称,因为它们是惯用的并且工作良好 b) 编写 post-incf、post-decf 版本,例如 (defmacro post-incf (x) `(prog1 ,x (incf ,x)) 之类的东西。
就我个人而言,我不认为这会特别有用,但是 ymmv。
对于预增量,已经有 incf,但您可以定义自己的
(define-modify-macro my-incf () 1+)
对于后增量,您可以使用它(来自 fare-utils):
(defmacro define-values-post-modify-macro (name val-vars lambda-list function)
"Multiple-values variant on define-modify macro, to yield pre-modification values"
(let ((env (gensym "ENV")))
`(defmacro ,name (,@val-vars ,@lambda-list &environment ,env)
(multiple-value-bind (vars vals store-vars writer-form reader-form)
(get-setf-expansion `(values ,,@val-vars) ,env)
(let ((val-temps (mapcar #'(lambda (temp) (gensym (symbol-name temp)))
',val-vars)))
`(let* (,@(mapcar #'list vars vals)
,@store-vars)
(multiple-value-bind ,val-temps ,reader-form
(multiple-value-setq ,store-vars
(,',function ,@val-temps ,,@lambda-list))
,writer-form
(values ,@val-temps))))))))
(defmacro define-post-modify-macro (name lambda-list function)
"Variant on define-modify-macro, to yield pre-modification values"
`(define-values-post-modify-macro ,name (,(gensym)) ,lambda-list ,function))
(define-post-modify-macro post-incf () 1+)
尽管我肯定会记住这些评论和注意事项 西蒙 他的帖子中的评论,我真的认为 用户10029的方法仍然值得一试,因此,只是为了好玩,我尝试将其与已接受的答案结合起来,以使 ++x 运算符工作(即将 x 的值增加 1)。试一试!
解释:好的旧 SBCL 不会编译他的版本,因为必须在调度字符查找表上显式设置“+”符号 make-dispatch-macro-character
, ,并且在评估变量之前仍然需要宏来传递变量的名称。所以这应该可以完成这项工作:
(defmacro increment (variable)
"The accepted answer"
`(incf ,variable))
(make-dispatch-macro-character #\+) ; make the dispatcher grab '+'
(defun |inc-reader| (stream subchar arg)
"sets ++<NUM> as an alias for (incf <NUM>).
Example: (setf x 1233.56) =>1233.56
++x => 1234.56
x => 1234.56"
(declare (ignore subchar arg))
(list 'increment (read stream t nil t)))
(set-dispatch-macro-character #\+ #\+ #'|inc-reader|)
看 |inc-reader|
的 文档字符串 获取使用示例。(密切)相关的文档可以在这里找到:
- http://clhs.lisp.se/Body/f_set__1.htm
- http://clhs.lisp.se/Body/f_mk_dis.htm#make-dispatch-macro-character
这种实现的结果是像 +123 这样的数字条目不再被理解(调试器用 no dispatch function defined for #\Newline
)但进一步的解决方法(甚至避免)似乎是合理的:如果你仍然想坚持这个,也许最好的选择不是使用 ++ 作为前缀,而是使用 ## 或任何其他更 DSL 的解决方案
干杯!
安德烈斯
这应该可以解决问题,但我不是 lisp 大师。
(defmacro ++ (variable)
`(setq ,variable (+ ,variable 1)))