Macros 如何实现push宏?

Macros 如何实现push宏?,macros,common-lisp,Macros,Common Lisp,有人能帮我理解如何将push实现为宏吗?下面的朴素版本对place表单求值两次,并在求值元素表单之前执行此操作: (defmacro my-push (element place) `(setf ,place (cons ,element ,place))) 但如果我尝试按以下方式修复此问题,那么我将设置到错误的位置: (defmacro my-push (element place) (let ((el-sym (gensym)) (place-sym (g

有人能帮我理解如何将
push
实现为宏吗?下面的朴素版本对place表单求值两次,并在求值元素表单之前执行此操作:

(defmacro my-push (element place)
  `(setf ,place (cons ,element ,place)))
但如果我尝试按以下方式修复此问题,那么我将
设置到错误的位置:

(defmacro my-push (element place)
   (let ((el-sym    (gensym))
         (place-sym (gensym)))
     `(let ((,el-sym    ,element)
            (,place-sym ,place))
        (setf ,place-sym (cons ,el-sym ,place-sym)))))

CL-USER> (defparameter *list* '(0 1 2 3))
*LIST*
CL-USER> (my-push 'hi *list*)
(HI 0 1 2 3)
CL-USER> *list*
(0 1 2 3)

我如何才能在不进行两次评估的情况下设置正确的位置?

看看现有的一个(至少在SBCL中)是如何工作的,我发现:

* (macroexpand-1 '(push 1 *foo*))

(LET* ((#:G823 1) (#:NEW822 (CONS #:G823 *FOO*)))
  (SETQ *FOO* #:NEW822))
T
所以,我想,把你的版本和它产生的东西结合起来,你可能会:

(defmacro my-push (element place)
   (let ((el-sym  (gensym))
         (new-sym (gensym "NEW")))
     `(let* ((,el-sym  ,element)
             (,new-sym (cons ,el-sym ,place)))
        (setq ,place ,new-sym)))))
一些意见:

  • 这似乎适用于
    setq
    setf
    。根据您实际试图解决的问题(我认为重新编写
    push
    不是实际的最终目标),您可能会选择其中一个

  • 请注意,
    place
    仍然会得到两次评估。。。尽管它至少在计算
    元素
    之后才这样做。你真的需要避免双重评估吗?(考虑到内置的
    推送功能不起作用,我想知道你是否/如何能够……尽管我在花大量时间思考之前写了这篇文章。)考虑到这是一个需要评估为“”的东西,也许这是正常的

  • 使用
    let*
    而不是
    let
    允许我们在设置
    、新sym
    时使用
    、el sym
    。这将移动到
    cons
    发生的位置,以便在
    的第一次求值中,放置
    ,以及在
    的求值之后,元素
    。关于评估顺序,这可能会满足您的需要

  • 我认为第二个版本的最大问题是,
    setf
    确实需要对传入的符号进行操作,而不是对
    gensym
    符号进行操作


  • 希望这有助于。。。(我自己对这一切还有些陌生,所以我在这里做一些猜测。)

    正确地做这件事似乎有点复杂。例如,SBCL 1.0.58中推送的代码为:

    (defmacro-mundanely push (obj place &environment env)
      #!+sb-doc
      "Takes an object and a location holding a list. Conses the object onto
      the list, returning the modified list. OBJ is evaluated before PLACE."
      (multiple-value-bind (dummies vals newval setter getter)
          (sb!xc:get-setf-expansion place env)
        (let ((g (gensym)))
          `(let* ((,g ,obj)
                  ,@(mapcar #'list dummies vals)
                  (,(car newval) (cons ,g ,getter))
                  ,@(cdr newval))
             ,setter))))
    
    因此,阅读上的文档似乎很有用

    作为记录,生成的代码看起来相当不错:

    推入一个符号:

    (push 1 symbol)
    
    扩展到

    (LET* ((#:G906 1) (#:NEW905 (CONS #:G906 SYMBOL)))
      (SETQ SYMBOL #:NEW905))
    
    (LET* ((#:G909 1)
           (#:SYMBOL908 SYMBOL)
           (#:NEW907 (CONS #:G909 (FIRST #:SYMBOL908))))
      (SB-KERNEL:%RPLACA #:SYMBOL908 #:NEW907))
    
    推入可设置的功能(假设
    symbol
    指向列表列表):

    扩展到

    (LET* ((#:G906 1) (#:NEW905 (CONS #:G906 SYMBOL)))
      (SETQ SYMBOL #:NEW905))
    
    (LET* ((#:G909 1)
           (#:SYMBOL908 SYMBOL)
           (#:NEW907 (CONS #:G909 (FIRST #:SYMBOL908))))
      (SB-KERNEL:%RPLACA #:SYMBOL908 #:NEW907))
    
    所以,除非你花一些时间去研究setf和它的公司,否则这看起来相当神秘(即使在研究它们之后也可能如此)。中的“广义变量”一章也可能有用


    提示:如果编译自己的SBCL(没有那么难),请将
    --fancy
    参数传递给
    make.sh
    。通过这种方式,您将能够快速查看SBCL中函数/宏的定义(例如,使用Emacs+SLIME中的M-.in)。显然,不要删除这些源代码(您可以在
    install.sh
    之后运行
    clean.sh
    ,以节省90%的空间)。

    查看SBCL源代码,我发现它们使用一次性宏来处理rplaca(有关更多信息,请参阅此链接)。我会调查rplaca以进一步了解。@PaulNathan您能提供您正在查看的SBCL版本和源位置吗?因为我在
    src/code/early setf.lisp
    (SBCL 1.0.58)中看到了,它不会只使用一次
    。很有趣。HyperSpec说位置表单只计算一次,但看起来SBCL知道从符号中获取两次值是无害的。对于可能有副作用的位置表单,它会:CL-USER>(宏扩展-1'(push 4(my list func))(LET*(#:G31 4)(#:G30(CONS):G31(MY-LIST-FUNC)))#:G30(FUNCALL#’(SETF MY-LIST-FUNC)#:G30))。这种形式的SETF是否以某种方式避免了第二次求值?当我实际运行上述函数时,我得到了“Undefined function(SETF MY-LIST-FUNC)”。看起来,通过禁止无法自行设置的place表单,避免了双重求值问题。这很有意义。