【问题标题】:Writing a ++ macro in Common Lisp在 Common Lisp 中编写 ++ 宏
【发布时间】:2008-09-15 18:47:57
【问题描述】:

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

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

但这在尝试使用它时给了我一个简单类型错误。什么使它起作用?

【问题讨论】:

标签: macros lisp common-lisp


【解决方案1】:

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

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

【讨论】:

    【解决方案2】:

    前面的两个答案都有效,但它们为您提供了一个您称之为宏的宏

    (++ 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)。

    【讨论】:

      【解决方案3】:

      语法(++ 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) 只出现一次。

      【讨论】:

      • @JoshuaTaylor 由define-modify-macro 创建的宏返回新的更新值。由于这是incf 需要返回的内容,因此很容易。写一个pincf 和define-modify-macro 是否同样容易,这并不明显,其中要求是返回以前在该位置的值。
      【解决方案4】:

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

      【讨论】:

      • 这会计算 number 两次。 Kaz's answer 展示了如何避免这种情况。
      【解决方案5】:

      在语义上,前缀运算符 ++ 和 -- 在诸如 c++ 之类的语言中或在普通 lisp 中等效的 incf/decf 中。如果你意识到这一点,并且像你的(不正确的)宏一样,实际上是在寻找一个句法改变,那么你已经被展示了如何使用像 `(incf ,x) 这样的反引号来做到这一点。甚至已经向您展示了如何让读者绕过这个来获得更接近非 lisp 语法的东西。不过,这就是问题所在,因为这些都不是一个好主意。一般而言,使一种语言更接近于另一种语言的非惯用编码并不是一个好主意。

      然而,如果你真的在寻找语义,你已经得到了前面提到的前缀版本,但后缀版本在语法上并不容易匹配。你可以用足够多的阅读器hacky来做到这一点,但它不会很漂亮。

      如果这就是你要找的东西,我建议 a) 坚持使用 incf/decf 名称,因为它们是惯用的并且工作良好 b) 编写 post-incf、post-decf 版本,例如(defmacro post-incf (x) `(prog1 ,x (incf ,x)) 之类的东西。

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

      【讨论】:

      • 提及prog1 本身就足以成为这篇文章的理由。用CL很久了,早就忘记了。
      【解决方案6】:

      对于pre-increment,已经有了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+)
      

      【讨论】:

        【解决方案7】:

        虽然我肯定会记住 simon cmets 在他的帖子中的评论和提醒,但我真的认为 user10029 的方法仍然值得一试,所以,只是为了好玩,我尝试将它与接受的答案结合起来,以使 ++x 运算符工作(即,将 x 的值增加 1)。试试看!

        解释:老旧的 SBCL 不会编译他的版本,因为必须在 dispatch-char 查找表上使用 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 解决方案

        干杯!

        安德烈斯

        【讨论】:

          【解决方案8】:

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

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

          【讨论】:

          • 这在所有情况下都不会按预期工作。正如您所说,“变量”会被计算两次,如果表达式有副作用,这不是用户所期望的。例如。看看你的宏如何扩展这个相当合理的调用:(++ (aref some-vector (++ some-index)))
          • 如果variable 不是变量(或符号宏),这也不起作用,因为setq 不适用于非变量。例如,有了这个,你不能做(++ (car list))。
          猜你喜欢
          • 2011-03-08
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-01-18
          • 2013-07-04
          • 2019-11-16
          • 2016-10-27
          相关资源
          最近更新 更多