无法更改列表
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)))))
请注意,我在这里只扩展了 setf
。 defun
和 dolist
都变成了非常可怕的扩展,使代码更难读。系统完全展开表格,因此运行的代码没有宏。
setq
更新绑定,因此当它更新 stack
时,它更新的就是它,而不是 *some-stack*
。如果你这样做 (stack-push *some-stack* "a")
:
- 您传递 函数
*some-stack*
和 "a"
。它评估其论点。
*some-stack*
计算出具有 cons
单元格的地址 A,例如。 ("b")
.
"a"
是一个驻留在地址 B 的文字
- 参数被绑定到新绑定。
stack
指向 A,元素指向 B。
- 因为
element
指向 B (not (listp element)) ; ==> t
并且代码遵循结果。
- in
(setf stack (cons element stack))
是在运行时更改为 (setq stack (cons element stack))
之前。
(cons element stack)
以 B 和 A 作为参数调用 cons
。 returns 地址为 C 的新单元格 ("a" "b")
setq
更新以便绑定 stack
指向 C。
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
扩展器,这样 setf
和 push
就可以工作。
作为对 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))
你可以看到问题所在:setf
对 foo
一无所知,所以它只是在调用 #'(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))
例如,我有函数 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)))))
请注意,我在这里只扩展了 setf
。 defun
和 dolist
都变成了非常可怕的扩展,使代码更难读。系统完全展开表格,因此运行的代码没有宏。
setq
更新绑定,因此当它更新 stack
时,它更新的就是它,而不是 *some-stack*
。如果你这样做 (stack-push *some-stack* "a")
:
- 您传递 函数
*some-stack*
和"a"
。它评估其论点。 *some-stack*
计算出具有cons
单元格的地址 A,例如。("b")
."a"
是一个驻留在地址 B 的文字
- 参数被绑定到新绑定。
stack
指向 A,元素指向 B。 - 因为
element
指向 B(not (listp element)) ; ==> t
并且代码遵循结果。 - in
(setf stack (cons element stack))
是在运行时更改为(setq stack (cons element stack))
之前。 (cons element stack)
以 B 和 A 作为参数调用cons
。 returns 地址为 C 的新单元格("a" "b")
setq
更新以便绑定stack
指向 C。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
扩展器,这样 setf
和 push
就可以工作。
作为对 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))
你可以看到问题所在:setf
对 foo
一无所知,所以它只是在调用 #'(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))