通过计划中的价值混淆解决

时间:2018-12-09 17:19:46

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

请考虑以下来自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在称为(lambda (amount) ... )的新环境中返回一个过程(e2)(将变量balance的绑定包含在内),并将该过程绑定到{{1 }}。

现在,说我打电话:

x

其中

 (f x)

1 。我听说Scheme是按价值传递的。这是否意味着当 (define (f y) (y 25)) 创建新环境f时,它将在e3上绑定x值的副本吗?

2 。也就是说,y(现在)持有的值(输入y的正文之后)是f持有的lambda的副本吗?

3 。因此,我们现在有了两个变量,全局变量xx中的y,每个变量都包含一个引用e3内部事物的过程?

4 。如果我是正确的话,e2x持有的程序是否像指向y的指针一样?

3 个答案:

答案 0 :(得分:2)

如果按值传递,则会评估函数的参数并将其值绑定到函数参数。

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

如果一个值很简单,例如一个整数,那么该整数会“复制”到新环境中分配的某些内存单元中;如果它更复杂,例如一个闭包(一个编译函数),则可以认为将该对象的“引用”复制到新环境中。

答案 1 :(得分:1)

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

angular

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

现在20test的值已更改,因此,如果您有其他入口点对x做过其他工作,则它将作为对象工作。 x就是这样工作的。

make-withdraw

上面的代码返回一个闭包,它的顶级环境绑定了(define x (make-withdraw 100)) balanced,当调用它的数量时,它将100set!结束,然后返回除非资金耗尽,否则为新值。

balance

这将使一个环境容纳(f x) ,这是一个封闭的环境,其封闭环境完好无损,并且绑定x会在调用结束时消失。它不会复制x,但是在x启动时,它永远不会依赖f是什么,因为它已经传递了值而不是名称。因此,x与始终调用(f x)相同。没变化!

答案 2 :(得分:1)

我发现思考这一问题的最佳方法是根据绑定进行思考,而不是仅仅作为绑定容器的环境或框架。

绑定

绑定是名称之间的关联。该名称通常被称为“变量”,而值则是变量的值。绑定的值可以是该语言可以讨论的任何对象。但是,绑定是幕后的事情(有时称为“不是一流的对象”):它们不是可以用语言表示的事物,而是可以用作模型一部分的事物语言的工作方式。所以绑定的值不能是绑定,因为绑定不是一流的:语言不能谈论绑定。

关于绑定有一些规则:

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

显然,需要对这些规则进行详尽的阐述(尤其是在全局绑定和前向引用绑定方面),并且要正式,但是这些足以理解发生了什么。特别是,我并不是真的不需要花费很多时间来担心环境:一点代码的环境只是它可以访问的一组绑定,因此,不必担心环境,而只需要担心绑定。

按值致电

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

  • 该过程无法更改原始绑定-这是因为该过程仅具有它的值,而没有绑定本身,并且绑定不是一流的,因此您不能通过传递绑定本身来作弊作为值;
  • 如果值本身是可变对象(数组和cons是通常可变的对象示例,数字是非可变对象的示例),则该过程可以使该对象发生突变。

有关绑定规则的示例

因此,这是这些规则的一些示例。

(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. fn绑定的值调用val绑定的值;
  3. 返回val绑定的值。

请注意,对fn的调用无论做什么 ,它都不能更改val的绑定,因为它无权访问它。因此,通过查看call-something的定义,您可以知道:如果它完全返回(如果对fn的调用未返回,则可能不返回) ,它将返回其第二个参数的值。这种保证就是“按值调用”的含义:支持其他调用机制的语言(例如Fortran)不能总是保证这一点。

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

这里有四个绑定:outer是一个顶级绑定,其值是一个过程,该过程在被调用时将为x创建一个绑定,其值是其参数。然后,它创建另一个称为inner的绑定,其值是另一个过程,该过程在被调用时为x的创建一个 new 绑定。参数,然后返回该绑定的值加1。 outer然后使用其对x的绑定值来调用此内部过程。

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

以下是用显式inner表示的先前代码(如果lambda是递归的,则不会等效):

(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中的。这段代码执行了“如果您能看到绑定,就总是可以看到它”的规则:通过调用val创建的make-counter的绑定在返回的过程中一直可见该过程存在(并且只要存在绑定,该过程就存在),并且它还会使用set!使绑定发生突变。

为什么没有环境?

SICP中的

chapter 3介绍了“环境模型”,其中在任何时候都存在一个由一系列框架组成的环境,每个框架都包含绑定。显然,这是一个很好的模型,但是它引入了三种类型的东西-环境,环境中的框架和框架中的绑定-其中两种是完全无形的。至少对于绑定,您可以通过某种方式来掌握它:可以在代码中看到它的创建过程,也可以看到对其的引用。因此,我不想考虑这两种额外的事情,而这些事情是您永远无法获得的。

但是,这是一个在实践中没有区别的选择:单纯地考虑绑定问题对我有帮助,而考虑环境,框架和绑定方面的思想则可能对其他人有更多帮助。

速记

在接下来的内容中,我将使用速记来讨论绑定,尤其是顶级绑定:

  • 'x是一个过程,...'意味着'x是绑定的名称,其值是一个过程,当被调用时,...';
  • 'y是...'意味着'y是绑定的名称,其值是...';
  • x调用y'意味着'用x命名的绑定值调用y命名的绑定的值'; < / li>
  • '...将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"(但 not 不会使balance绑定发生突变,因此您可以使用较小的金额重试:真实的银行可能会此时从balance绑定中吸取一些钱是罚款)。

现在

(define x (make-withdraw 100))

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

(define (f y) (y 25))

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

25

因此,(f x) f一起调用,x是(绑定到)上面构造的过程。在x中,f绑定到此过程(而不是副本),然后使用参数y调用此过程。然后,此过程的行为如上所述,结果如下:

25

请注意:

  • 在此过程中,没有将一流的对象复制到任何地方:没有创建过程的“副本”;
  • 在此过程中,没有任何一流的对象发生突变;
  • 在此过程中创建绑定(后来绑定变得不再可用,因此可以销毁);
  • 在此过程中,一个绑定被重复突变(每次调用一次);
  • 我在任何地方都无需提及“环境”,它只是从代码中特定位置可见的一组绑定,我认为这不是一个非常有用的概念。

我希望这有道理。


上述代码的详细版本

您可能想做的是撤消您帐户上的交易。一种方法是返回以及撤销新余额的过程,该过程将撤消上一次交易。这是执行此操作的过程(此代码在Racket中):

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

使用此过程创建帐户时,调用它会返回两个值:第一个是新余额,或者是(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"为真。如果您了解这是如何工作的,那么您可能会了解绑定。