在许多lisp实现中,push是一个像这样的宏:
(push new list)
;; equal to
(setf list (cons new list))
但是setf无法修改参数,例如:
(defun add-item (new list)
(push new list))
不起作用,因为函数参数不是原始符号。
为什么不推动这样的工作:
(defun my-push (new list)
(setcdr list (cons (car list)
(cdr list)))
(setcar list new)
list)
然后push可以在函数的参数上工作。 有没有理由让lisp推动这种方式工作?
我只是emacs lisp和sicp计划的新手。
答案 0 :(得分:5)
破坏性push
函数的一个问题是它无法在空列表nil
上运行。这有点像“交易破坏者”。
请注意push
宏,虽然它是一个命令式构造,它改变了保存列表头部的存储位置的值,但它避免了改变该列表的结构。
对于使用push
和pop
超过局部变量的列表处理代码,很容易理由;你不必担心变异列表结构可能导致的错误。
答案 1 :(得分:2)
请记住,多个符号(或其他值)可能指向该列表或其子列表。
如果push
按您的建议方式工作,那么您可以更改超过指定符号的值。
考虑:
(setq l1 '(foo bar))
(setq l2 (append '(baz) l1))
如果你现在推动'通过操纵指向的cons小区的l1
和car
来cdr
,您还可以修改l2
的值。
当然,有时候这正是你想要做的事情;但是push
不是这样做的,显然你不能重新定义push
以这种方式工作而不会在其他代码中造成不必要的副作用。
答案 2 :(得分:1)
有几种类型的突变。一个是对象,您可以将car
或cdr
或cons
更改为不同的对象。 cons
之前和之后将具有相同的地址,因此指向它的每个变量都将指向相同的地址,但对象会发生变化。
(defparameter *mutating-obj* (list 1))
(defparameter *mutating-obj2* *mutating-obj*)
(setf (cdr *mutating-obj*) '(2))
在这里,您可以改变对象,以便两个变量仍然指向已更改的相同值。在评估其中任何一个时,您会看到(1 2)
。
要知道,因为我们改变了对象,所以在开始时该值永远不会是()
,因为它不是car
和cdr
可以变异的东西。
在变量上使用setf
,您可以将变量视为值的地址位置。因此setf
将改变该位置而不是值本身。
(defparameter *var1* '(1 2 3))
(defparameter *var2* *var1*)
现在我们有两个指向同一列表的变量。如果我这样做:
(push 0 *var2*)
然后*var2*
更改了指针,使其指向一个以0开头的新列表,并具有前一个值的尾部。这不会更改*var1*
,它仍然指向之前的值*var2*
。
当你用一个值调用一个函数时,值被绑定为一个新变量,对它执行push
会做同样的事情,改变那个变量,而不是碰巧指向同一个值的其他变量。
push
的常见用法是从空列表开始并向其添加元素。设置car
和cdr
不能将空列表更改为具有给定值的单个元素列表。指向nil
的所有变量都不会更改,因此如果您的数据结构具有从未使用过的head元素,则使用rplacd
((setf (cdr var) ...)
)的方法有效:
(defun make-stack ()
(list 'stack-head))
(defun push-stack (element stack)
(assert (eq (car stack) 'stack-head))
(setf (cdr stack) (cons element (cdr stack)))
stack)
(defun pop-stack (stack)
(let ((popped (cadr stack)))
(setf (cdr stack) (cddr stack))
popped))
除非你专门设计它,否则这不起作用,因此push
确实需要改变变量而不是值,因为它始终有效。 (除了认为它会改变价值观的初学者)
答案 3 :(得分:0)
阅读更多代码, (如https://www.emacswiki.org/emacs/ListModification。) 我发现lisp程序员通常会复制并修改原始列表, 然后将其分配给原始列表符号。 也许这就是lisp的风格。
我来自javascript,它总是修改原始数组, 但很少复制它。 谢谢你的回答。