例如,我有函数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*
没有影响。你能解释一下原因吗?
答案 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)))))
请注意,我这里只展开了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
,因此代码遵循结果。(setf stack (cons element stack))
中的(setq stack (cons element stack))
之前。 (cons element stack)
以B和A作为参数调用cons
。返回地址为C ("a" "b")
setq
更新,以便绑定stack
指向C. 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
- 能够。尝试使用文字扩展为不可运行的代码。对于类型,您可以创建自己的setf
扩展器,以便setf
和push
有效。
答案 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))
您可以看到问题: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))