【问题标题】:Pass by Object Reference in Common Lisp在 Common Lisp 中传递对象引用
【发布时间】:2020-09-25 19:43:20
【问题描述】:

Peter Seibel 在 Practical Common Lisp 中写道:“Common Lisp 中的所有值至少在概念上都是对对象的引用。”

我用以下代码尝试了这个概念:

(setf x 5)
(setf y x)
(print x) % output: x is 5
(print y) % output: y is 5
(setf x 6)
(print x) % output: x is 6
(print y) % output: y is 5

如果 Lisp 是按对象引用传递的,y 应该指向 x,因此将 x 更改为 6 也应该将 y 更改为 6。但事实并非如此。看起来 Lisp 是按对象值传递的。有人可以帮忙解释发生了什么吗?

【问题讨论】:

  • (setf y x)y 设置为x 引用的对象。它不会将y 设置为对象引用本身。
  • @RainerJoswig 那么如何分配对象引用本身呢?
  • Common Lisp 不支持该功能。有一个 Lisp 概念称为“定位词”,但标准 Common Lisp 不支持此概念。

标签: lisp common-lisp pass-by-reference pass-by-value


【解决方案1】:

Lisp 实现中通常不引用小整数。说所有值都是 Common Lisp 中的引用是不正确的。在怀疑值是引用的情况下进行编程通常是更安全的假设,更符合编写正确的代码。

但是,您的示例与作为引用实现的小整数一致。这并不能证明他们不是。

如果变量x 包含一个像5 这样的整数,然后我们用(setf x 4) 分配给x,我们不会将对象5 变异为4。我们正在改变变量绑定x:我们已经用新值4 覆盖了之前在x 中的5 值。

即使我们使用正向引用到堆中的对象(如 cons 单元格),这也会起作用:

(setf x '(1 . 2))
(setf y x)
y -> (1 . 2)
(setf x '(4 . 5))
y -> (1 . 2)

xy 是独立变量,并且独立地持有对 cons 单元格的引用。 x 最初持有对 (1 . 2) 对象的引用。我们将x 分配给y,所以现在y 也持有对(1 . 2) 的引用。它有自己的参考副本。那么当我们分配(4 . 5) 时,x 的引用被(4 . 5) 的引用覆盖,但y 不受影响。为什么会这样?

我们如何证明 conses 使用引用语义是通过改变单元格本身:

(setf x (cons 1 2)) ;; we better not use a literal any more!
x -> (1 . 2)
(setf y x)
y -> (1 . 2)

;; now mutate

(rplaca x 10)
(rplacd x 20)

x -> (10 . 20)
y -> (10 . 20)

由于对存储在x 中的单元格进行变异会使变异出现在存储在y 中的单元格上,因此我们知道xy 必须持有对同一对象的引用。

现在问题来了:我们不能用整数执行这个测试,因为整数是不可变的!没有类似于rplaca 的函数会破坏代表1 的实际位,并将它们转换为10

eq 函数没有帮助,因为它所能做的就是确认两个值是同一个对象:

(setf x 5)
(setf y x)

(eq x y) -> ?

如果此eq 调用返回T,则xy 是同一个对象。我说 if 是因为 ANSI Common Lisp 离开了这个实现特定的。允许实现产生 NIL!

然而,它在小整数(fixnums)被直接打包成一个值的实现中产生T,而不是指向装箱堆对象的指针(流行的实现方法)。也就是说,像4 这样的值在它出现的任何地方都被认为是一个对象,即使它是通过复制一个直接保存代表4 的位模式的值来传播的。

所以它或多或少归结为您只需要知道您的实现是如何工作的就可以确定哪些类型的对象是引用。

【讨论】:

    【解决方案2】:

    [这个答案大多重复别人:我在他们存在之前就开始了,不想扔掉它。]

    Lisp(特别是 Common Lisp,但也包括其他)严格按值调用。特别是如果f是一个函数,那么

    (let ((x ...))
      (let ((y x))
        (f x)
        (eql y x)))
    

    是真的。

    但是有很多值是引用,或者换句话说,很多类型的对象都有标识。

    这通常意味着大多数值是内存中对象的地址。所以,几乎可以肯定

    (let ((x (cons nil nil)))
      (let ((y x))
        ...))
    

    那么x的值在实现上就是cons构造的对象的地址,y的值也是。这意味着,例如给定

    (defun mutate-cdr (c to)
      (setf (cdr c) to))
    

    然后

    (let ((x (cons nil nil)))
      (let ((y x))
        (mutate-cdr x 3)
        (cdr y)))
    

    3

    但是,在某些特殊情况下,某些类型的对象会立即表示:通常是小数字、字符和其他一些东西。立即表示的所有类型的对象都是不可变的,因此没有太大区别,尽管与此相关的相等性有复杂的规则。

    不过,Lisp 足够强大,可以根据需要支持引用调用,只需一点语法:

    (declaim (inline locf (setf locf)))
    
    (defun locf (loc)
      (funcall loc 'get))
    
    (defun (setf locf) (new loc)
      (funcall loc 'set new))
    
    (defmacro locative (place)
      `(lambda (op &optional val)
         (ecase op
           ((get) ,place)
           ((set) (setf ,place val)))))
    

    现在给出

    (defun mutate-loc (loc new)
      (setf (locf loc) new))
    

    然后

    (let ((x 1))
      (let ((y (locative x)))
        (mutate-loc y 4)
        x))
    

    4。当然,这适用于任何地方:

    (let ((x (vector 1 2 3)))
      (let ((y (locative (aref x 2))))
        (mutate-loc y 4)
        x))
    

    是第二个元素为4的向量。

    在一些实现中,诸如此类的东西被内置到语言中:但是我不记得它们是如何工作的细节。

    【讨论】:

      【解决方案3】:

      正如@Kaz 提到的, 像字符串、数字、符号这样的文字,它们以一种特殊的方式实现,因此在分配时大多被复制/重新创建。

      但是一旦你生成了一些更复杂的结构——组合结构—— setf 将引用该位置。

      (defparameter *x* (list 3))
      (setf *y* *x*)
      *x* ;; (3)
      *y* ;; (3)
      
      ;; mutate *x*
      (setf (car *x*) 5)
      *x* ;; (5)
      *y* ;; (5)
      
      ;; if you mutate *x* by letting things be there and adding sth e.g.
      (setf (cdr *x*) 7)
      *x* ;; (5 . 7)
      *y* ;; (5 . 7)
      
      (setf (cdr *x*) (list 1 2))
      *x* ;; (5 1 2)
      *y* ;; (5 1 2)
      
      ;; however, completely refedine *x*
      ;; then *y* will still refer to the old *x*
      ;; maybe because the creation of a new object (list 5 6) 
      ;; makes the symbol *x* be bound to a completely new place 
      ;; - where (list 5 6) was generated.
      (setf *x* (list 5 6))
      *x* ;; (5 6)
      *y* ;; (5 1 2)
      
      ;; at least we can say, as long as we are mutation on the structures
      ;; of the cons cells - *y* will follow *x*'s mutations
      
      

      你想做什么,可以通过使用函数引用你的变量来实现(使用闭包!)

      (defparameter *q* 3)
      (defun y () *q*)
      

      从现在开始,无论您使用(setf *q* <whatever>) 分配给*q* 的任何值, 您将可以通过(y) 获得。

      但是,要使用(setf (y) ...) 更改*q*,您必须首先定义相应的setf 函数(并以它修改然后直接*q* 的方式定义它。

      为了更有一种调用变量而不是函数的感觉, 也许可以创建一个 readermacro,例如对于@y,然后扩展为(y)。以及要定义的宏(并在后台定义相应的setf 函数...)。这样一来,至少会有处理变量的错觉......

      只是我的想法。

      【讨论】:

        【解决方案4】:

        今天想了一些类似的东西(在 C 中通过引用样式传递),并检查了 Rainer 对 Variable references in lisp 的回答,我意识到还有另一种方法。

        对于引用传递问题,全局变量是答案之一,部分原因是它们是特殊变量。我们可以将一个局部变量定义为特殊变量,并将其传递给一个引用函数。

        我们希望有一个函数来改变参数的引用值。

        如果你写下面的函数,那将是不正确的,因为这个函数更新的是局部变量文本,而不是引用:

        (defun display-and-update (text)
          (print text)
          (setf text (concatenate 'string text ", updated.")))
        

        测试:

        (let ((my-text "test"))
          (display-and-update my-text)
          (print my-text))
        

        输出:

        > "test"
        > "test"
        

        相反,我们应该使用不同的显式机制来显示我们的目的(例如,在 C 中作为指针运算符 * 和地址 / 引用运算符 & )。这样做的方法是使用符号(在 C 中,最接近的等价物是变量,而不是文字,因为我们也不能在 C 文字上使用指针和地址运算符)。

        现在,让我们重新定义函数以使用引用:

        (defun display-and-update (sym-text)
          (print (symbol-value sym-text))
          (set sym-text (concatenate 'string
                                     (symbol-value sym-text)
                                     ", updated.")))
        

        这里,符号值函数是我们的“解引用”运算符,就像 C 中的 * 一样。set 是 C 中的 *_ = 运算符,它设置符号的值。

        现在,我们如何将局部符号(我实际上应该说词法变量)传递给这个函数的问题由特殊变量解决,因为词法变量不能被另一个范围访问 - 例如函数 display- and-udpate 的范围。上面的相同调用稍作修改以实现此目的:

        (let ((my-text "test"))
          (declare (special my-text))
          (display-and-update 'my-text)
          (print my-text))
        

        输出:

        > "test"
        > "test, updated."
        

        您可能已经注意到,我们使用 'my-text 而不是 my-text 调用 display-and-update,因为 my-text 会评估值并通过“test”,这会失败。 ' 这里就像 C 中的 & 运算符。

        现在,为偏离原始问题而道歉,但它们是相关的,至少为了更好地理解。

        要让一个变量引用另一个变量,同样在 C 中,我们可以这样写:

        int a = 3;
        int *b = &a;
        *b = 5; // a is 5
        

        在 common-lisp 中实现同样的效果:

        (defparameter a 3)
        (defparameter b 'a)
        (set b 5) ;; a is 5
        

        现在,我们可以再次看到 set 函数实际上不是 C 中的 = 运算符,而是 *var = 运算符,因为它首先获取符号的值并设置它。 例如,如果您创建 b 以等于其他值,则可能为 nil:

        (defparameter a 3)
        (defparameter b nil)
        ;; (set b 'a) ;; this won't work, as it's trying to set the symbol nil's value to 'a
        ;; instead:
        (setf b 'a) ;; this is a better match for C's = operator
        

        会达到同样的效果。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2015-11-11
          • 2013-07-26
          • 1970-01-01
          • 2019-11-03
          • 2016-04-26
          • 1970-01-01
          相关资源
          最近更新 更多