在Common Lisp中编写++宏

在Common Lisp中编写++宏

Writing a ++ macro in Common Lisp

我一直在尝试编写一个Lisp宏,出于语义上的原因,该宏在其他编程语言中将表现为++。 我尝试以几种不同的方式执行此操作,但是它们似乎都不起作用,并且都被解释器接受,所以我不知道我是否具有正确的语法。 我对如何定义的想法是

1
2
(defmacro ++ (variable)
  (incf variable))

但这在尝试使用它时给了我SIMPLE-TYPE-ERROR。 什么会使它起作用?


请记住,宏返回要求值的表达式。为此,您必须反引号:

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

前面的两个答案都可以,但是它们为您提供了一个宏,您将其称为

1
(++ varname)

而不是我怀疑您想要的varname ++或++ varname。我不知道您是否真的可以获得前者,但对于后者,您可以执行读取宏。由于它是两个字符,因此调度宏可能是最好的。未经测试,因为我没有方便的运行口齿不清,但类似:

1
2
3
4
(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有何不同?"的人,这都会降低可读性。

如果您想要一个简单的后增量,请尝试以下操作:

1
2
3
4
5
6
(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)也具有该属性。因此,简单的宏不够用:

1
2
(defmacro pincf (place &optional (increment 1))
  `(prog1 ,place (incf ,place ,increment))

为此,我们必须求助于Lisp的"分配位置分析器",称为get-setf-expansion,以获得可以使我们的宏正确编译访问权限的材料:

1
2
3
4
5
6
7
8
9
10
11
(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的扩展可能包含特定于实现的代码。这并不意味着我们的宏不可移植!)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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))。这必须被评估一次!

1
2
3
4
5
6
7
8
[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)仅出现一次。


从语义上讲,前缀运算符++和-用c ++之类的语言或常见lisp中等效的incf / decf表示。如果您意识到这一点,并且像(不正确的)宏一样,实际上正在寻找语法更改,那么您已经被展示了如何使用`(incf,x)这样的反引号来做到这一点。甚至还向您展示了如何使读者对此有所帮助,以使之更接近非lisp语法。不过这很麻烦,因为这些东西都不是一个好主意。通常,使一种语言更接近于另一种语言的非惯用编码并不是一个好主意。

但是,如果实际上正在寻找语义,您已经获得了前面提到的前缀版本,但是后缀版本在语法上很难匹配。您可以使用足够多的阅读器黑客来做到这一点,但这并不是一件很漂亮的事情。

如果这就是您要查找的内容,我建议a)坚持使用incf / decf名称,因为它们是惯用的并且可以正常工作; b)编写inf-incf后的版本,例如(defmacro post-incf(x) `(prog1,x(incf,x))之类的东西。

就个人而言,我不认为这特别有用,但ymmv。


对于预增量,已经有incf,但是您可以使用

1
(define-modify-macro my-incf () 1+)

对于后增量,您可以使用以下命令(来自fare-utils):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(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+)


尽管我一定会牢记simon在其帖子中的评论和注意,但我确实认为user10029的方法仍然值得一试,因此,为了好玩,我尝试将其与公认的答案结合起来, ++ x运算符的工作(即,将x的值增加1)。试试看!

说明:好的旧版SBCL不会编译其版本,因为必须在<-x0>上的dispatch-char查找表上显式设置'+'符号,并且在评估它之前仍然需要宏来传递变量的名称。因此,这应该可以完成工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(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 #
ewline
跳转),但进一步的解决方法(甚至避免)似乎是合理的:如果您仍然希望坚持使用此方法,也许最好的选择是不要以++为前缀,而是使用##或其他任何类似DSL的解决方案

干杯!

安德烈斯


这应该可以解决问题,但是我不是精打细算的专家。

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

推荐阅读