【问题标题】:How do I globally change a variable value within function in lisp如何在lisp的函数中全局更改变量值
【发布时间】:2013-10-21 09:52:59
【问题描述】:

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

如果我有

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

a 在调用 mutate 后会变成 5,不管之前是什么。

我知道带有列表的元素是可能的(大部分是副作用) In common-lisp, how do I modify part of a list parameter from within a function without changing the original list? 但我想知道如何为整个列表执行此操作。

【问题讨论】:

    标签: lisp common-lisp argument-passing


    【解决方案1】:

    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;
    }
    

    代码打印三个,因为mutate 中的a 是一个仅存在于mutate 中的变量。仅仅因为它与全局 a 共享一个名称并不意味着改变一个会改变另一个。在这段代码中,您可以更改mutate 的变量a 的值的唯一位置是mutate。您没有“更改 [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 设为cons 单元格,其car3,那么您可以传递cons 并修改其car 的值:

    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 Overview of Places and Generalized Reference

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

    在 Common Lisp 中,您可以使用 setf 分配给地点。 suggestions that sds gave 的共同点是,您可以通过使用全局变量符号作为setf 的位置或使用symbol-value 来修改全局变量的值。也就是说,在诸如(defparameter *a* 3) 之类的定义之后,*a*(symbol-value '*a*) 都是地方,您可以在其中存储*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 em>,在Common Lisp, reference to value and actual value的回答中简要提及。一旦您知道要搜索的术语,就很容易找到有关定位词的更多信息,包括 Lisp 机器手册中的 Chapter 13. Locatives 和 Common Lisp 的各种重新实现,包括 Alan Crowe's,它以长注释开头,以 (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)。该代码当然值得一读!

    【讨论】:

      【解决方案2】:

      使用函数

      如果你想使用一个函数,你必须传递符号本身:

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

      请注意,mutatesymbol 参数已被引用。

      使用宏

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

      symbol 参数不再需要被引用。

      讨论

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

      宏接收the code (the whole form),它们转换并将新代码传递给编译器(或解释器)。因此宏可以决定如何修改它们的参数(在这种情况下称为places or generalized references)。

      【讨论】:

      • 不幸的是,这不起作用:如果我将 c 设置为空列表并尝试在函数中更改它:(mutate c (remove-duplicates (merge 'list a b #'
      • @smihael:你说的是哪个版本?宏?功能?
      【解决方案3】:

      Here is a Common Lisp module 让您“获取”在 Lisp 中被视为“位置”的任何存储位置的地址:不仅是变量,还包括结构槽或数组等。

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

      例如,(ref (foo-accessor (cdr (aref a 4))) 将创建对存储位置的引用,该引用是通过跟踪数组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))
      

      【讨论】:

      • +1 用于展示使用get-setf-expansion 来避免多个评估问题的解决方案! -1 不包括代码。如果该链接失效,则此答案不再对任何人有帮助。 :(如果代码进入答案,我会投赞成票。:)
      • @JoshuaTaylor 我把代码放进去了。但你知道,如果每个网页都是复制而不是链接,以防万一链接失效,这会破坏万维网的概念。
      • 我同意,网络的用处在于它是一个网络。也就是说,Stack Overflow 是关于为问题提供持久、规范的答案。你的答案不是“仅链接”的答案,但即使有额外的内容,如果链接失效,它对稍后在这里找到它的人也没有用。参见,例如,Are answers that just contain links elsewhere really “good answers”?。不过,我确实认识到,这不是仅提供链接的答案;不仅如此。
      猜你喜欢
      • 2013-10-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-03-12
      • 2022-10-01
      • 1970-01-01
      相关资源
      最近更新 更多