3

我想知道是否有任何方法可以用 LISP 中的指针来模仿 C 行为。在C语言中,如果你改变一个变量的值,那个指针所指向的,它具有全局效果(即该值也将在函数之外被改变)。

所以如果我有

(defun mutate ( a ) 
   (some-magic-function a 5)
)

a 会在调用 mutate 后变为 5,无论之前是什么。

我知道带有列表的元素是可能的(大部分是副作用) 在 common-lisp 中,如何在不更改原始列表的情况下从函数中修改列表参数的一部分? 但我想知道如何为整个列表做这件事。

4

3 回答 3

8

C代码的讨论

sds 的答案解决了问题的要点,但看起来您正在模拟的 C 代码中发生的事情确实有点混乱:

我想知道是否有任何方法可以用 LISP 中的指针来模仿 C 行为。在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;
}

代码打印三个,因为ainmutate是一个仅存在于 中的变量mutate。仅仅因为它与全局共享名称a并不意味着改变一个会改变另一个。这段代码中唯一可以更改mutate's 变量值的地方amutate. 您没有“更改 [a] 指针指向的变量的 [the] 值”的选项。您可以做的是将指针传递给变量的值,通过该指针修改值,然后在值中观察结果。这将对应于这个 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一个is的cons单元格,那么您可以传递它并修改它的值:car3conscar

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 中没有地址运算符,因此您不能完全模拟 C 代码,如果您想使用这种方法,您总是需要以某种方式“包装”值. 您可以使用 Common Lisp 中的现有结构,例如 cons 单元格、向量或您可以找到的任何其他结构。

广义参考

虽然它没有 C 风格的指针,但 Common Lisp 定义了一种非常广泛的方式来引用内存位置以进行读写,称为Generalized Reference

5.1.1 场所概述和概括参考

广义引用是一种形式的使用,有时称为位置,就好像它是一个可以读写的变量一样。地点的价值是地点形式评估的对象。可以使用 setf 更改位置的值。Common Lisp 中没有定义绑定位置的概念,但允许实现通过定义这个概念来扩展语言。

在 Common Lisp 中,您可以使用setf. sds 给出的建议有一个共同点,即您可以通过使用全局变量符号作为 的位置setf或使用 来修改全局变量的值symbol-value。也就是说,在定义之后,例如 (defparameter *a* 3)both*a*(symbol-value '*a*)are您可以在其中存储新值的位置*a*。因此,我宁愿写一个带有变量名的宏placeand value,这样就很清楚任何地方都可以用作参数:

(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返回一个可以使用一个或两个参数调用的函数。第一个参数应该是一个符号,要么是reador write,而第二个参数应该在第一个参数是 时提供write,是要存储在该位置的新值。使用 调用时read,将返回该位置的值。使用 调用时write,将存储并返回一个新值。

不过,这里的多重评估存在一些问题。例如,如果您要执行以下操作,请记住(print 2)返回值2

(make-pointer (aref some-array (print 2)))

2 每次使用指针读取或写入时,您最终都会打印,这可能是不希望的。我不知道这个问题是否需要解决,但请继续阅读一些可能的方法来避免这种情况。

在对类似问题(How to mutate global variable passing to and mutated inside function?)进行了一些研究之后,值得注意的是 Lisp Machines(运行 Lisp Machine Lisp,而不是 Common Lisp)有一个更像 C 指针的概念,称为locatives,在 Common Lisp 的答案中简要提到 ,引用 value 和 actual value。一旦你知道要搜索的术语,就很容易找到更多关于定位词的信息,包括第 13 章。来自 Lisp 机器手册的定位词和 Common Lisp 的各种重新实现,包括Alan Crowe 的,它以长注释开头,以 (promising ) 简明总结:

;;; 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)每次都进行评估。该代码当然值得一读!

于 2013-10-21T12:46:19.397 回答
4

使用函数

如果要使用函数,则必须传递符号本身:

(defun mutate (symbol value)
  (setf (symbol-value symbol) value))
(mutate 'foo 42)
foo
==> 42

请注意,symbol参数 tomutate被引用。

使用宏

(defmacro mutate (symbol value)
  `(setf ,symbol ,value))
(mutate foo 42)
foo
==> 42

symbol不再需要引用该论点。

讨论

在 Lisp 中,参数是按值传递的,这意味着函数看到的是其参数的,而不是存储它的位置。因此,作为(foo (! 5)),调用的函数只能看到数字,并且绝对无法知道该数字是由函数返回(第一种情况),是文字(第二种情况)还是存储在变量中(第三种情况)。(foo 120)(foo x)120

宏接收它们转换代码(整个形式)并将新代码传递给编译器(或解释器)。因此,宏可以确定如何修改它们的参数(在这种情况下称为位置广义引用)。

于 2013-10-21T11:56:36.507 回答
1

这是一个 Common Lisp 模块,它允许您“获取”在 Lisp 中被视为“位置”的任何存储位置的地址:不仅是变量,还有结构槽或数组等。

您所取地址的存储位置可以被任意复杂的表达式引用,就像在 C 中一样。

例如,(ref (foo-accessor (cdr (aref a 4)))将创建对通过追逐 array 的第五个元素a(一个单元格)获得的存储位置的引用,获取它的cdr,然后应用于foo-accessor从那里检索到的对象。

当您取消引用引用时,它不会每次都遍历整个链,而是直接到达内存位置。

一个简单的用法是:

(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);

请注意,我们在这里得到的比 C 指针安全得多。这些指针永远不会变坏。只要这种类型的引用存在于某个地方,那么持有那个地方的对象就不会消失。例如,我们可以安全地返回对词法变量的引用。这些指针并不便宜:解引用涉及对闭包的函数调用,而不是简单地追逐内存地址。需要访问存储位置的信息存储在闭包的词法变量绑定环境中,并且必须调用该闭包,以便执行执行获取和设置的代码。

类似 Lisp 的语言可以支持更接近真实指针的东西作为扩展。这会使垃圾收集器复杂化。例如,假设您可以有一个指针,它只是一个直接指向某个数组的第 53 个元素的地址(不涉及重量级的词法闭包技巧)。垃圾收集器必须跟踪这些指针,以免回收通过“内部”指针以这种方式引用的数组。在没有这样的扩展的情况下,Lisp 垃圾收集器永远不必担心不会发生的内部指针。堆上的对象总是由指向其正确基地址的指针引用。内部指针意味着垃圾收集器必须解决“哪个对象包含这个地址?”的问题。该解决方案可能涉及搜索动态数据结构,例如树。(这是在 Linux 内核中采用的方法,它需要能够将任意虚拟地址映射到struct vma描述保存该地址的虚拟内存映射的描述符。)

执行:

;;;
;;; Lisp references: pointer-like place locators that can be passed around.
;;; Source: http://www.kylheku.com/cgit/lisp-snippets/plain/refs.lisp
;;; 
;;; How to use:
;;;
;;; Produce a reference which "lifts" the place designated
;;; by form P:
;;;
;;;   (ref p)
;;;
;;; Dereference a reference R to designate the original place:
;;;
;;;   (deref r)
;;;   (setf (deref r) 42) ;; store new value 42
;;;
;;; Shorthand notation instead of writing a lot of (deref)
;;; Over FORMS, A is a symbol macro which expands to 
;;; (DEREF RA), B expands to (DEREF RB):
;;;
;;;   (with-refs ((a ra) (b rb) ...)
;;;     
;;;     ... forms)
;;; 
(defstruct ref 
  (get-func) 
  (set-func))

(defun deref (ref) 
  (funcall (ref-get-func ref))) 

(defun (setf deref) (val ref) 
  (funcall (ref-set-func ref) val)) 

(defmacro ref (place-expression &environment env)
  (multiple-value-bind (temp-syms val-forms 
                        store-vars store-form access-form)
                        (get-setf-expansion place-expression env)
    (when (cdr store-vars)
      (error "REF: cannot take ref of multiple-value place"))
    `(multiple-value-bind (,@temp-syms) (values ,@val-forms)
       (make-ref
         :get-func (lambda () ,access-form)
         :set-func (lambda (,@store-vars) ,store-form)))))

(defmacro with-refs ((&rest ref-specs) &body forms) 
  `(symbol-macrolet 
     ,(loop for (var ref) in ref-specs 
            collecting (list var `(deref ,ref))) 
     ,@forms))
于 2013-10-23T23:13:24.537 回答