我想知道是否有任何方法可以使用LISP中的指针模拟C行为。在C中如果你改变一个变量的值,该指针指向它,它就具有全局效果(即该值也将在函数外部改变)。
所以,如果我有
(defun mutate ( a )
(some-magic-function a 5)
)
无论以前是什么,调用mutate后都会变成5。
我知道带有列表的元素是可能的(很多副作用) In common-lisp, how do I modify part of a list parameter from within a function without changing the original list? 但我想知道如何为整个清单做到这一点。
答案 0 :(得分:8)
sds's answer解决了问题的要点,但看起来对于你正在模仿的C代码中发生的事情有点混淆:
我想知道是否有办法模仿C行为 LISP中的指针。在C中,如果更改变量的值,则为 指针指向,它具有全局效果(即值将是 在功能之外也改变了。)
请考虑以下内容,我认为这与您提供的Lisp代码非常相似:
#include<stdio.h>
int a = 3;
int mutate( int a ) {
return a = 5;
}
int main() {
mutate( a ); /* or mutate( 8 ) or anything other argument */
printf( "%d\n", a ); /* prints 3 */
return 0;
}
代码打印3,因为a
中的mutate
是仅存在于mutate
内的变量。仅仅因为它与全局a
共享一个名称并不意味着改变一个将改变另一个。此代码中您可以更改mutate
变量a
的值的唯一位置是mutate
。您没有选择“更改[a]指针所指向的变量的值”。你可以做的是将指针传递给变量的值,通过该指针修改值,然后在值中观察结果。这将对应于此C代码:
#include<stdio.h>
int a = 3;
int mutate( int *a ) {
return (*a = 5);
}
int main() {
mutate( &a );
printf( "%d\n", a ); /* prints 5 */
return 0;
}
您也可以使用您喜欢的任何间接方式在Common Lisp中执行此类操作。例如,如果您a
cons
car
的{{1}}格式为3
,那么您可以传递cons
并修改其car
的值1}}:
CL-USER> (defparameter *a* (cons 3 nil))
*A*
CL-USER> (defun mutate (cons)
(setf (car cons) 5))
MUTATE
CL-USER> (mutate *a*)
5
CL-USER> (car *a*)
5
你在Lisp中没有一个address-of运算符,所以你不能完全模拟C代码,你总是需要以某种方式“包装”这个值,如果你想使用这种方法。您可以使用Common Lisp中的现有结构,例如cons单元格,向量或其他任何可以找到的结构。
虽然它没有C风格的指针,但Common Lisp定义了一种非常宽泛的方式来引用读写内存位置,称为Generalized Reference。
5.1.1 Overview of Places and Generalized Reference
广义参考是使用表格,有时称为a 放置,好像它是一个可以读写的变量。该 地点的价值是地方形式评估的对象。该 可以使用setf更改地点的值。绑定的概念 Common Lisp中没有定义一个地方,但实现是 允许通过定义这个概念来扩展语言。
在Common Lisp中,您可以使用setf
分配到地点。 suggestions that sds gave共同的共同点是,您可以使用全局变量符号作为setf
或symbol-value
的位置来修改全局变量的值。也就是说,在(defparameter *a* 3)
*a*
和(symbol-value '*a*)
之类的定义为 places 之后,您可以在其中存储*a*
的新值。因此,我宁愿编写一个变量名为place
和value
的宏,以便明确任何地方都可以用作参数:
(defmacro mutate (place value)
`(setf ,place ,value))
因为词汇变量也是地方,还有另一个尚未考虑的选项。您可以使用词法闭包创建函数,这些函数将提供与C样式指针相同的功能。
(defmacro make-pointer (place)
`(lambda (op &optional value)
(ecase op
((read) ,place)
((write) (setf ,place value)))))
(let* ((x 3)
(xp (make-pointer x)))
(funcall xp 'write 5) ; write a new value to x
(list (funcall xp 'read) ; read the value from x through xp
x)) ; read the value from x directly
;=> (5 5)
在此代码中,make-pointer
返回一个可以使用一个或两个参数调用的函数。第一个参数应该是一个符号,read
或write
,第二个参数(在第一个参数为write
时应该提供)是一个存储在该位置的新值。使用read
调用时,将返回该地点的值。使用write
调用时,会存储并返回一个新值。
但是,这里有多个评估存在一些问题。例如,如果您要执行以下操作,请记住(print 2)
返回值2
:
(make-pointer (aref some-array (print 2)))
您每次使用指针读取或写入时都会打印2
,这可能是不可取的。我不知道这个问题是否需要解决,但请继续阅读一些可能的方法来避免这个问题。
在对一个类似的问题(How to mutate global variable passed to and mutated inside function?)进行一些研究之后,值得注意的是,Lisp Machines(运行Lisp Machine Lisp,而不是Common Lisp)有一个更像C指针的概念叫做 locatives ,在Common Lisp, reference to value and actual value的答案中简要提及。一旦你知道了要搜索的术语,就可以很容易地找到更多关于locative的信息,包括Lisp机器手册中的Chapter 13. Locatives和Common Lisp的各种重新实现,包括Alan Crowe's,其中有一条长注释结束(有希望的)简明摘要:
;;; The basic idea is to use closures
稍后(来源读得非常好),你会得到:
;;; It looks as though we are done
;;; now we can translate C code
;;; &x = (addr x), *x = (data x)
但有一点需要注意
;;; The trouble is, we have a multiple evaluation bug.
Crowe接着展示如何使用get-setf-expansion
来创建记住如何访问位置并将值存储到的函数,而无需每个评估(print 2)
时间。该代码当然值得一读!
答案 1 :(得分:4)
如果你想使用一个函数,你必须传递符号本身:
(defun mutate (symbol value)
(setf (symbol-value symbol) value))
(mutate 'foo 42)
foo
==> 42
请注意引用symbol
的{{1}}参数。
mutate
不再需要引用(defmacro mutate (symbol value)
`(setf ,symbol ,value))
(mutate foo 42)
foo
==> 42
参数。
Lisp中的参数是passed by value,这意味着函数会看到其参数的值,而不是它存储的位置。
因此,调用symbol
,(foo (! 5))
,(foo 120)
的函数只能看到数字(foo x)
,并且绝对无法知道函数是否返回此数字(第1种情况) ,是一个文字(第二种情况)或存储在一个变量(第三种情况)。
宏接收the code (the whole form) 转换并将新代码传递给编译器(或解释器)。因此,宏可以确定如何修改它们的参数(在这种情况下称为places or generalized references)。
答案 2 :(得分:1)
Here is a Common Lisp module,它允许您“获取”Lisp中被视为“位置”的任何存储位置的“地址”:不仅是变量,还有结构的插槽或数组等。
您的地址所在的存储位置可以通过任意复杂的表达式引用,就像在C中一样。
例如,(ref (foo-accessor (cdr (aref a 4)))
将创建对通过追踪数组a
的第五个元素获取的存储位置的引用,该数组是一个单元格,取其cdr
,然后应用{ {1}}从那里检索的对象。
当您取消引用引用时,它不会每次都通过整个链,而是直接到达内存位置。
一个简单的用法是:
foo-accessor
请注意,我们在这里获得的内容比C指针更安全。这些指针永远不会变坏。只要这种类型的引用存在于某个地方,那么拥有该地方的对象就不会消失。例如,我们可以安全地返回对词法变量的引用。这些指针并不便宜:dereferencing涉及对闭包的函数调用,而不是简单地追逐内存地址。需要访问存储位置的信息存储在闭包的词法变量绑定环境中,并且必须调用该闭包,以便执行执行获取和设置的代码段。
类似Lisp的语言可以支持更接近实际指针的扩展。这会使垃圾收集器复杂化。例如,假设你可以有一个指针,它只是一个直接针对某个数组的第53个元素的地址(不涉及重量级的词法封闭技巧)。垃圾收集器必须跟踪这些指针,以便不回收通过“内部”指针以这种方式引用的数组。在没有这种扩展的情况下,Lisp垃圾收集器永远不必担心内部指针,这些内部指针不会发生。堆上的对象总是通过指向其正确基址的引用来引用。内部指针意味着垃圾收集器必须解决“哪个对象包含此地址?”的问题。该解决方案可以涉及搜索诸如树的动态数据结构。 (这是Linux内核中采用的方法,需要能够将任意虚拟地址映射到描述保存该地址的虚拟内存映射的(defun mutate-to-five (ptr) | void mutate_to_five(int *a)
(setf (deref ptr) 5)) | { *ptr = 5; }
|
(defparameter a 42) | int a = 42;
| /*...*/
(mutate-to-five (ref a)) | mutate_to_five(&a);
描述符。)
struct vma