无法更改列表

时间:2018-05-06 15:07:49

标签: lisp common-lisp clisp

例如,我有函数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*没有影响。你能解释一下原因吗?

2 个答案:

答案 0 :(得分:3)

带有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. (setf stack (cons element stack))中的
  8. 在运行时更改为(setq stack (cons element stack))之前。
  9. (cons element stack)以B和A作为参数调用cons。返回地址为C ("a" "b")
  10. 的新单元格
  11. setq更新,以便绑定stack指向C.
  12. stack-push返回最后一个评估值,这是setq的结果,它是第二个参数C.
  13. 在函数中没有提到*some-stack*。绑定永远不会引用或更新。只有stack是要指向新值的更新。

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

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

    有一个名为push的表单。 不是功能。你可以看到它的作用:

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

    因此,对于push工作,你需要第二个参数是setf - 能够。尝试使用文字扩展为不可运行的代码。对于类型,您可以创建自己的setf扩展器,以便setfpush有效。

答案 1 :(得分:2)

作为对西尔维斯特答案的一个注释,这是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))