【问题标题】:Can't change list无法更改列表
【发布时间】:2018-10-16 11:48:38
【问题描述】:

例如,我有函数 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*。你能解释一下为什么吗?

【问题讨论】:

标签: lisp common-lisp clisp


【解决方案1】:

作为对 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

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

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

【讨论】:

  • 您仍在多次评估该地点 (stack)。它可能有非纯子形式,这会导致问题。编写一个返回新值的常规函数​​会更容易,并使用DEFINE-MODIFY-MACRO为其创建一个修改宏。
  • @jkiiski 我还没有看到:堆栈最多出现两次(因为typecase 只评估两种情况之一)并且只评估其中一种通常,另一个是setf 中的第一个子表单。
  • 例如,(setf (foo (random 10)) (+ 10 (foo (random 10)))) 计算 (random 10) 两次。
  • @jkiiski:哦,是的,我现在觉得自己很愚蠢。我已经相当广泛地修改了答案(有信用)。谢谢!
【解决方案2】:

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. 参数被绑定到新的绑定。 stack指向A,元素指向B。
  5. 因为element 指向B (not (listp element)) ; ==> t 并且代码跟在结果后面。
  6. (setf stack (cons element stack)) 中是在运行时更改为 (setq stack (cons element stack)) 之前。
  7. (cons element stack) 以 B 和 A 作为参数调用 cons。返回地址为 C ("a" "b") 的新单元格
  8. setq 更新以便绑定 stack 指向 C。
  9. stack-push 返回最后一个评估值,这是 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 工作。

【讨论】:

    猜你喜欢
    • 2020-03-08
    • 1970-01-01
    • 2011-03-24
    • 2021-12-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多