在Common Lisp中编写一个++宏

6

我一直在尝试编写一个Lisp宏,以实现其他编程语言中++的等效功能,以便于语义。 我尝试了几种不同的方法,但是它们似乎都不能正常工作,并且所有方法都被解释器接受,所以我不知道是否具有正确的语法。 我对如何定义这个宏的想法是:

(defmacro ++ (variable)
  (incf variable))

但是当我尝试使用它时,会出现“简单类型错误”。有什么方法可以使它正常工作吗?

不是重复,但相关:编写像 incf 这样的破坏性宏或函数? - Joshua Taylor
8个回答

17

记住宏返回要评估的表达式。为了做到这一点,您必须使用反引号:

(defmacro ++ (variable)
   `(incf ,variable))

14

之前的两个答案都是有效的,但它们会给你一个宏,你需要调用它:

(++ 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)一样被读取


11
语法 (++ 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函数存储回数组位置。请注意,此存储调用使用临时变量而不是原始表达式AI(INCF I)仅出现一次。


1
@JoshuaTaylor define-modify-macro 创建的宏返回新的、更新后的值。因为这正是 incf 需要返回的,所以很容易实现。但是,使用 define-modify-macro 编写一个 pincf 并不明显,因为其要求返回之前在该位置上的值。 - Kaz

10
我强烈建议不要为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)))

2
这会对number进行两次评估。Kaz的答案展示了如何避免这种情况。 - Joshua Taylor

7
语义上,在像C++这样的语言中,前缀运算符++和--与通用Lisp中的incf/decf等效。如果您意识到这一点,并且像您(不正确的)宏一样,实际上正在寻找语法更改,那么您已经知道如何使用反引号(`)进行操作,例如`(incf,x)`。 甚至还向您展示了如何使读取器绕过此以获取更接近非Lisp语法的内容。但问题在于,这两件事都不是一个好主意。通常,为了使一种语言更像另一种语言而进行的非习惯用法编码并不是一个好主意。
然而,如果您实际上正在寻找语义,那么您已经注意到了前缀版本,但后缀版本很难在语法上匹配。您可以通过足够的读取器技巧来完成它,但这不会很优雅。
如果您正在寻找这样的内容,我建议a)坚持使用incf / decf名称,因为它们是惯用的且工作良好;b)编写后缀incf、post-decf版本,例如(defmacro post-incf (x) `(prog1 ,x (incf ,x)))之类的东西。
个人而言,我不认为这将特别有用,但您可能有不同看法。

1
提到 prog1 已经足以成为这篇文章的原因。我已经使用 CL 很长时间了,但很久以前就忘记它的存在了。 - Mars

5

对于前置自增,已经有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+)

2
尽管我一定会记住simon在他的帖子中所做的评论和提醒,但我真的认为user10029的方法仍然值得一试,所以,只是为了好玩,我试图将它与被接受的答案结合起来,使++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|docstring以获取使用示例。 (紧密) 相关的文档可以在此处找到: 这种实现的后果是不再理解数字输入,例如+123(调试器跳入no dispatch function defined for #\Newline),但进一步的解决方法(甚至避免)似乎是合理的:如果您仍然想坚持使用这个,也许最好的选择不是采用++作为前缀,而是##或任何其他更DSL-ish的解决方案。
祝福!
安德烈斯

-2

这应该可以解决问题,但我不是Lisp大师。

(defmacro ++ (variable)
  `(setq ,variable (+ ,variable 1)))

4
在所有情况下,这个方法可能不会按预期工作。正如你所说,“变量”会被评估两次,如果表达式具有副作用,则用户不会期望这种情况发生。 例如,看看你的宏如何扩展这个非常合理的调用: (++ (aref some-vector (++ some-index))) - HD.
如果variable不是变量(或符号宏),这也不起作用,因为setq不能处理非变量。例如,使用此方法,您无法执行(++ (car list)) - Joshua Taylor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接