无法更改列表

Can't change list

例如,我有函数 stack-push 将元素压入堆栈。

(defun stack-push (stack element)
    (if (not (listp element))
        (setf stack (cons element stack))
        (dolist (current-el (reverse element))
            (setf stack (cons current-el stack)))))

但是当我像 (stack-push *some-stack* '(a b c d e)) 那样调用它时,它不会影响 *some-stack*。你能解释一下为什么吗?

setf 带有符号,如 (setf stack (cons element stack)) 扩展为 (setq stack (cons element stack))。这通常发生在您创建函数时。你的函数变成这样:

(defun stack-push (stack element)
  (if (not (listp element)
      (setq stack (cons element stack))
      (dolist (current-el (reverse element))
        (setq stack (cons current-el stack)))))

请注意,我在这里只扩展了 setfdefundolist 都变成了非常可怕的扩展,使代码更难读。系统完全展开表格,因此运行的代码没有宏。

setq 更新绑定,因此当它更新 stack 时,它更新的就是它,而不是 *some-stack*。如果你这样做 (stack-push *some-stack* "a"):

  1. 您传递 函数 *some-stack*"a"。它评估其论点。
  2. *some-stack* 计算出具有 cons 单元格的地址 A,例如。 ("b").
  3. "a" 是一个驻留在地址 B
  4. 的文字
  5. 参数被绑定到新绑定。 stack 指向 A,元素指向 B。
  6. 因为 element 指向 B (not (listp element)) ; ==> t 并且代码遵循结果。
  7. in (setf stack (cons element stack)) 是在运行时更改为 (setq stack (cons element stack)) 之前。
  8. (cons element stack) 以 B 和 A 作为参数调用 cons。 returns 地址为 C 的新单元格 ("a" "b")
  9. setq 更新以便绑定 stack 指向 C。
  10. stack-push returns 最后一个计算值,它是 setq 的结果,它是第二个参数 C。

函数中没有提到*some-stack*。绑定永远不会被引用或更新。只有 stack 是更新指向的新值。

由于这是一个函数,您可以使用文字调用您的函数,如果参数是要更新的变量,这通常不会起作用。

(stack-push '("b") "a") ; ==> ("a" "b")

有一种形式叫做push。它不是函数。你可以看到它的作用:

(macroexpand '(push "a" *some-stack*)) 
; ==> (setq *some-stack* (cons "a" *some-stack*))

因此,要使 push 正常工作,您需要第二个参数是 setf-able。用文字尝试它会扩展为无法运行的代码。对于类型,您可以制作自己的 setf 扩展器,这样 setfpush 就可以工作。

作为对 Sylwester 回答的注释,这里是你的 stack-push 的一个版本,乍一看看起来是正确的,但实际上有一个丑陋的问题(感谢 jkiiski 指出这一点!),然后是一个更简单的版本仍然有问题,最后是一个没有问题的更简单版本的变体。

这是初始版本。这与您的签名一致(它采用不能是列表的单个参数或参数列表,并根据它看到的内容决定做什么)。

(defmacro stack-push (stack element/s)
  ;; buggy, see below!
  (let ((en (make-symbol "ELEMENT/S")))
    `(let ((,en ,element/s))
       (typecase ,en
         (list
          (setf ,stack (append (reverse ,en)
                               ,stack)))
         (t
          (setf ,stack (cons ,en ,stack)))))))

但是我更想用 &rest 参数来编写它,如下所示。这个版本更简单,因为它总是做一件事。虽然它仍然是越野车。

(defmacro stack-push* (stack &rest elements)
  ;; still buggy
  `(setf ,stack (append (reverse (list ,@elements)) ,stack)))

这个版本可以用作

(let ((a '()))
  (stack-push* a 1 2 3)
  (assert (equal a '(3 2 1))))

例如。而且它似乎有效。

多重评价

但是它不起作用,因为它可以对不应该被多重计算的东西进行多重计算。最简单的方法(我发现)是查看宏扩展是什么。

我有一个名为 macropp 的小实用函数来执行此操作:它会根据您的要求多次调用 macroexpand-1,并打印结果。要查看问题,您需要展开两次:首先展开 stack-push*,然后查看结果 seetf 会发生什么。第二个扩展依赖于实现,但你可以看到问题所在。这个示例来自 Clozure CL,它有一个特别简单的扩展:

? (macropp '(stack-push* (foo (a)) 1) 2)
-- (stack-push* (foo (a)) 1)
-> (setf (foo (a)) (append (reverse (list 1)) (foo (a))))
-> (let ((#:g86139 (a)))
     (funcall #'(setf foo) (append (reverse (list 1)) (foo (a))) #:g86139))

你可以看到问题所在:setffoo 一无所知,所以它只是在调用 #'(setf foo)。它小心翼翼地确保以正确的顺序评估子表单,但它只是以明显的方式评估第二个子表单,结果 (a) 被评估 两次 ,这是错误的:如果它有副作用,那么它们会发生两次。

所以解决这个问题的方法是使用 define-modify-macro 来解决这个问题。为此,您定义一个函数来创建堆栈,然后使用 define-modify-macro 来创建宏:

(defun stackify (s &rest elements)
  (append (reverse elements) s))

(define-modify-macro stack-push* (s &rest elements)
  stackify)

现在

? (macropp '(stack-push* (foo (a)) 1) 2)
-- (stack-push* (foo (a)) 1)
-> (let* ((#:g86170 (a)) (#:g86169 (stackify (foo #:g86170) 1)))
     (funcall #'(setf foo) #:g86169 #:g86170))
-> (let* ((#:g86170 (a)) (#:g86169 (stackify (foo #:g86170) 1)))
     (funcall #'(setf foo) #:g86169 #:g86170))

而且您可以看到现在 (a) 只计算了一次(而且您现在只需要一个级别的宏展开)。

再次感谢 jkiiski 指出错误。


macropp

为了完整起见,这里是我用来漂亮打印宏扩展的函数。这只是一个技巧。

(defun macropp (form &optional (n 1))
  (let ((*print-pretty* t))
    (loop repeat n
          for first = t then nil
          for current = (macroexpand-1 form) then (macroexpand-1 current)
          when first do (format t "~&-- ~S~%" form)
          do (format t "~&-> ~S~%" current)))
  (values))