【问题标题】:Pass by value confusion in SchemeScheme中的传递值混淆
【发布时间】:2019-05-10 17:01:29
【问题描述】:

考虑以下取自SICP的过程:

 (define (make-withdraw balance)
   (lambda (amount)
     (if (>= balance amount)
         (begin (set! balance 
                      (- balance amount))
                balance)
         "Insufficient funds")))

假设我说:

 (define x (make-withdraw 100))

make-withdraw 在名为 e2 的新环境中返回一个过程 ((lambda (amount) ... ))(包含变量 balance 的绑定),并将该过程绑定到全局框架中的 x

现在,假设我打电话:

 (f x)

在哪里

 (define (f y) (y 25))

1。我听说 Scheme 是按值传递的。这是否意味着f 在创建新环境e3 时,会在y 上绑定x 的值的副本?

2。即y(现在)持有的值(在输入f的主体之后)是x持有的lambda的副本?

3。所以我们现在有两个变量,全局中的xe3 中的y,每个变量都包含一个引用e2 内部事物的过程?

4。如果我是正确的,xy 持有的程序是否像指向e2 的指针?

【问题讨论】:

  • new-withdraw 是一个有一个参数的函数,但你调用它时没有参数。这应该会出错。
  • @Renzo,已编辑。谢谢
  • x 然后绑定到值 0,由调用 (new-withdraw 100) 返回。
  • @Renzo 我将程序更改为 make-withdraw,它现在返回一个 lambda。

标签: scheme lisp pass-by-value sicp shallow-copy


【解决方案1】:

我发现考虑这一点的最佳方式是考虑绑定,而不是环境或框架,它们只是绑定的容器。

绑定

绑定是 namevalue 之间的关联。该名称通常称为“变量”,其值就是变量的值。绑定的值可以是语言可以谈论的任何对象。然而,绑定是幕后的东西(有时这被称为“不是一流的对象”):它们不是可以用语言表示的东西,而是可以用作模型一部分的东西语言是如何工作的。所以绑定的值不能是绑定,因为绑定不是一等的:语言不能谈论绑定。

有一些关于绑定的规则:

  • 有创建它们的表单,其中最重要的两个是lambdadefine
  • 绑定不是一流的——该语言不能将绑定表示为值;
  • 绑定是,或者可能是,可变的——一旦绑定存在,你就可以改变它的值——这样做的形式是set!
  • 没有破坏绑定的操作符;
  • 绑定具有词法作用域 -- 一些代码可用的绑定是您可以通过查看它看到的绑定,而不是您必须通过运行代码来猜测的绑定,这可能取决于系统的动态状态;
  • 对于给定名称,只有一个绑定可以从给定的代码中访问 - 如果不止一个在词法上可见,则最里面的绑定会遮盖任何外部的;
  • 绑定有无限的范围 -- 如果绑定可用于某段代码,则它始终可用。

显然,这些规则需要详细阐述(尤其是关于全局绑定和前向引用绑定)并且是正式的,但这些足以理解会发生什么。特别是我不认为你需要花很多时间担心环境:一段代码的环境只是它可以访问的绑定集,所以不用担心环境,只需担心绑定。

按值调用

所以,“按值调用”的意思是,当您调用带有变量(绑定)参数的过程时,传递给它的是变量绑定的 ,不是绑定本身。然后该过程创建一个具有相同值的 new 绑定。由此得出两件事:

  • 原始绑定不能被过程更改 -- 这是因为过程只有它的值,而不是绑定本身,并且绑定不是一流的,因此您不能通过传递绑定本身来作弊作为值;
  • 如果值本身是一个可变对象(数组和 conses 是通常可变对象的示例,数字是不可变对象的示例),则该过程可以改变该对象。

绑定规则示例

所以,这里有一些这些规则的例子。

(define (silly x)
  (set! x (+ x 1))
  x)

(define (call-something fn val)
  (fn val)
  val))

> (call-something silly 10)
10

因此,我们在这里为sillycall-something 创建了两个顶级绑定,它们的值都是过程。 silly 的值是一个过程,当被调用时:

  1. 创建一个新绑定,其名称为x,其值为silly 的参数;
  2. 改变这个绑定,使其值加一;
  3. 返回此绑定的值,该值比调用它时使用的值大一。

call-something 的值是一个过程,当被调用时:

  1. 创建两个绑定,一个名为fn,一个名为val
  2. val绑定的值调用fn绑定的值;
  3. 返回val绑定的值。

请注意无论调用fn,它不能改变val的绑定,因为它无法访问它。所以你可以知道,通过查看call-something 的定义,如果它完全返回(如果对fn 的调用没有返回,它可能不会返回),它将返回它的第二个参数的值。这种保证就是“按值调用”的意思:支持其他调用机制的语言(例如 Fortran)不能总是保证这一点。

(define (outer x)
  (define (inner x)
    (+ x 1))
  (inner (+ x 1)))

这里有四个绑定:outer 是一个顶级绑定,它的值是一个过程,当它被调用时,它会为x 创建一个绑定,其值是它的参数。然后它创建另一个名为 inner 的绑定,其值是另一个过程,当它被调用时,它会为 x 创建一个 new 绑定到 its 参数,然后返回该绑定的值加一。然后outer 使用其与x 的绑定值调用此内部过程。

这里重要的是,在inner 中,x 有两个绑定,它们可能在词法上可见,但最接近的一个——inner 建立的那个——获胜,因为只有一个绑定因为一个给定的名称可以一次访问。

这是前面的代码(如果 inner 是递归的,这将不等效)用显式 lambdas 表示:

(define outer
  (λ (x)
    ((λ (inner)
       (inner (+ x 1)))
     (λ (x)
       (+ x 1)))))

最后是变异绑定的示例:

(define (make-counter val)
  (λ ()
    (let ((current val))
      (set! val (+ val 1))
      current)))

> (define counter (make-counter 0))
> (counter)
0
> (counter)
1
> (counter)
2

所以,在这里,make-counter(它的值是一个过程的绑定的名称,当被调用时)为val 建立一个新的绑定,然后返回它创建的过程。此过程创建了一个名为current 的新绑定,它捕获val 的当前值,改变val 的绑定以将其加一,并返回current 的值。这段代码练习了“如果你能看到一个绑定,你总是能看到它”的规则:通过调用make-counter 创建的val 的绑定对于它返回的过程是可见的,只要该过程存在(并且该过程至少存在于它的绑定中),并且它还会改变与set! 的绑定。

为什么不是环境?

SICPchapter 3 中引入了“环境模型”,其中任何时候都有一个环境,由一系列帧组成,每个帧都包含绑定。显然这是一个很好的模型,但是它引入了三种东西——环境、环境中的框架和框架中的绑定——其中两种是完全无形的。至少对于绑定,您可以通过某种方式获取它:您可以看到它是在代码中创建的,并且您可以看到对它的引用。所以我不想考虑这两种你永远无法处理的额外事物。

然而,这是一个在实践中没有任何区别的选择:纯粹从绑定的角度思考对我有帮助,从环境、框架和绑定的角度思考可能对其他人有更多帮助。

速记

在下文中,我将使用简写来讨论绑定,尤其是顶级绑定:

  • 'x is an procedure which ...'表示'x是绑定的名称,其值是一个过程,当被调用时,...';
  • 'y is ...'表示'y是绑定的名称,其值为...';
  • 'x is called with y'意思是'x命名的绑定值被y命名的绑定值调用';
  • '...将x绑定到...'的意思是'...创建一个名称为x的绑定,其值为...';
  • 'x'表示'x的值';
  • 等等。

像这样描述绑定很常见,因为完全显式的方式很痛苦:我已经尝试(但可能在某些地方失败)在上面完全显式。

答案

最后,在这个冗长的序言之后,这是您所提问题的答案。

(define (make-withdraw balance)
  (λ (amount)
    (if (>= balance amount)
        (begin (set! balance (- balance amount))
               balance)
        "Insufficient funds")))

make-withdrawbalance 绑定到它的参数并返回它创建的过程。这个过程,当被调用时:

  1. amount 绑定到它的参数;
  2. amountbalance 进行比较(它仍然可以看到,因为它在创建时可以看到);
  3. 如果有足够的钱,它会改变balance 绑定,将其值减少amount 绑定的值,并返回新值;
  4. 如果没有足够的钱,它会返回"Insuficient funds"(但不会改变balance绑定,所以你可以用更少的钱再试一次:真正的银行可能会吸走一些钱balance 绑定在这一点上很好)。

现在

(define x (make-withdraw 100))

x 创建一个绑定,其值是上述过程之一:在该过程中balance 最初是100

(define (f y) (y 25))

f 是一个过程(是一个绑定的名称,其值为一个过程,当被调用时)它将y 绑定到它的参数,然后用25 的参数调用它。

(f x)

所以,f 被调用,xx 被(绑定到)上面构造的过程。在f 中,y 绑定到此过程(而不是它的副本,绑定到它),然后使用25 的参数调用此过程。然后这个过程的行为如上所述,结果如下:

> (f x)
75
> (f x)
50
> (f x)
25
> (f x)
0
> (f x)
"Insufficient funds"

注意:

  • 在此过程中的任何地方都没有复制第一类对象:没有创建过程的“副本”;
  • 在此过程中的任何地方都不会发生一流对象的变异;
  • 在此过程中创建绑定(后来变得不可访问,因此可以被销毁);
  • 一个绑定在此过程中反复发生变异(每次调用一次);
  • 我不需要在任何地方提及“环境”,它们只是从代码中的某个点可见的一组绑定,我认为这不是一个非常有用的概念。

我希望这有点道理。


上述代码的更精细版本

您可能想要做的事情是撤销您帐户上的交易。一种方法是返回和新余额一样,撤销最后一笔交易的过程。这是一个执行此操作的过程(此代码位于Racket):

(define (make-withdraw/backout
         balance
         (insufficient-funds "Insufficient funds"))
  (λ (amount)
    (if (>= balance amount)
        (let ((last-balance balance))
          (set! balance (- balance amount))
              (values balance
                      (λ ()
                       (set! balance last-balance)
                       balance)))
            (values
             insufficient-funds
             (λ () balance)))))

当您使用此过程创建帐户时,调用它会返回两个值:第一个是新余额,或insufficient-funds 的值(默认为"Insufficient funds"),第二个是撤消交易的过程你刚刚做到了。请注意,它通过显式放回旧余额来撤消它,因为我认为在浮点运算存在的情况下,您不一定要依赖 (= (- (+ x y) y) x) 为真。如果您了解其工作原理,那么您可能了解绑定。

【讨论】:

  • @WillNess:谢谢,我已经解决了。不幸的是,可能还有其他无缘无故的更改,因为我在提交后稍微更改了它的更高版本。
  • 就其实质而言,我想评论一下,您的回答只是将“魔法”推向了“绑定”,这些实体非常神秘,不知何故是可变的。 SICP 实现试图澄清这一点,但它使可变 cons 单元格 成为一个未探索的谜团。所以我们终究要讲一个具体的实现。当然,“正常”内存是可以很自然地设置的(例如,数组条目,在 C 中等)
  • @WillNess:是的,我认为我的目标(实际上现在有一段关于此的内容)是拥有某种最低限度的魔法:你需要一些,但我不喜欢所有这些SICP 中的额外层(环境和框架)。 & 绑定几乎是有形的:您可以制作包装特定绑定并允许您设置它的东西,即使您看不到它。我正要补充,但答案已经太长了。
  • 嗯,这是一个品味问题,但作者的目的是澄清事情,我敢肯定。如果没有环境,我们如何实现解释器?浅绑定似乎更难以实现词法作用域。
  • @WillNess:是的,绝对是个人喜好问题,我希望答案能说明问题(现在)
【解决方案2】:

在按值传递的情况下,对函数的参数进行求值,并将它们的值绑定到函数参数。

因此,如果参数是表达式,则会对其进行求值,并将值绑定到参数。如果是绑定到值的标识符,则该值绑定到参数。

如果一个值很简单,比如一个整数,这个整数会被“复制”到新环境中分配的某个内存单元中,如果它是更复杂的东西,比如闭包(一个编译函数),你可以这样想该对象的“引用”会复制到新环境中。

【讨论】:

  • @OP 一个相关的概念是对于可能深度嵌套的列表,列表结构的浅拷贝与深拷贝。例如,Common Lisp 的copy-list 只复制顶层列表结构。也就是说,它创建了一个新的链接 cons 单元(节点)链,并用原始节点的内容一个一个地填充它们。 cs.cmu.edu/~dst/LispBook/book.pdf 谈论这个东西很多并且有很多图表作为插图。 ( :( ).
  • /经常谈论这些东西,并且有很多图表作为插图。/ --- 书名是:David Touretzky 的“COMMON LISP: A Gentle Introduction to Symbolic Computation”。
【解决方案3】:

按值传递并不意味着它不是对象的地址,因为它通常是。 C++ 是一种按值传递的语言。下面是一个传值属性的例子:

(define (test x)
  (set! x 10))

(define y 20)
(test y)

上面的代码永远不会改变y,因为x 是一个新的绑定,它恰好指向与y 相同的值,而(set! x 10) 使x 指向不同的值。 y 仍将指向原始值 20

现在在test 中,x 的值已更改,因此如果您有其他入口点使用x 执行其他操作,那么它将作为一个对象工作。这就是make-withdraw 的工作原理。

(define x (make-withdraw 100))

上面的代码返回一个闭包,它的顶级环境有 balanced 绑定到 100 并且当用一个金额调用它时,它将从它的闭包中 set! balance 然后返回新值,除非资金耗尽.

(f x)

这会创建一个保存x 的环境,这是一个完整的环境闭包,并且绑定x 在调用完成时会解散。它不会复制x,但是当f 启动时,它从不依赖x 是什么,因为它传递的是值,而不是名称。因此(f x) 与总是调用(x 25) 相同。没有变化!

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-05-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-06
    • 2015-04-27
    相关资源
    最近更新 更多