为什么lisp宏推只改变符号?
在许多Lisp实现,推是一个宏观的这个样子的:为什么lisp宏推只改变符号?
(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)
然后推可以在功能参数工作。 有没有任何理由让lisp推送这种方式工作?
我只是emacs lisp和sicp方案的新手。
您的破坏性push
函数的一个问题是它不能在空列表nil
上工作。这是一个“交易断路器”。
请注意,push
宏,虽然它是一个命令性的构造,它改变了保存列表头部的存储位置的值,但它避免了变换该列表的结构。
对于使用push
和pop
而不是局部变量的列表处理代码很容易推理;你不必关心由变异列表结构引起的可能的错误。
请记住,倍符号(或其他值)可能指向该列表或其子列表。
如果push
按照您的建议工作,那么您可以更改超过指定符号的值。
考虑:
(setq l1 '(foo bar))
(setq l2 (append '(baz) l1))
如果你现在“推”到l1
通过操纵car
和它指向的,你也将修改的l2
值的利弊细胞cdr
。
当然,有些时候这正是你想要做的;然而push
不是实现它的方法,显然你不能重新定义push
以这种方式工作而不会在其他代码中产生不需要的副作用。
谷歌后,读更多的代码, (如https://www.emacswiki.org/emacs/ListModification) 我发现口齿不清程序设计师通常复制并修改原始列表, 然后将其分配到原始列表符号。也许这是lisp的风格。
我来自JavaScript,它总是修改原始数组, 但很少复制它。 感谢您的回答。
有几种类型的突变。一个是对象,您可以在其中将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
变量将不会就此改变使用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
真的需要改变变量,而不是从那以后它的价值永远。 (除了认为它改变数值的初学者)
零条件可以通过一些额外的测试来避免。 – gholk
@gholk:如果你没有变量可访问性,怎么会这样呢? –
@gholk问题是'nil'是一个你不能改变的原子。你不能写一个'mypush'函数,使得'(mypush 1 nil)'将'nil'变成'(1)'。该函数当然可以返回该对象,但调用者必须捕获该返回值并更新所有包含该列表的地方。 – Kaz