如何在lisp中全局更改函数内的变量值

时间:2013-10-21 09:52:59

标签: lisp common-lisp argument-passing

我想知道是否有任何方法可以使用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? 但我想知道如何为整个清单做到这一点。

3 个答案:

答案 0 :(得分:8)

C代码的讨论

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共同的共同点是,您可以使用全局变量符号作为setfsymbol-value的位置来修改全局变量的值。也就是说,在(defparameter *a* 3) *a*(symbol-value '*a*)之类的定义为 places 之后,您可以在其中存储*a*的新值。因此,我宁愿编写一个变量名为placevalue的宏,以便明确任何地方都可以用作参数:

(defmacro mutate (place value)
  `(setf ,place ,value))

使用词法闭包模拟C变量指针

因为词汇变量也是地方,还有另一个尚未考虑的选项。您可以使用词法闭包创建函数,这些函数将提供与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返回一个可以使用一个或两个参数调用的函数。第一个参数应该是一个符号,readwrite,第二个参数(在第一个参数为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