首页 文章

在Scheme中传递 Value 混淆

提问于
浏览
1

请考虑从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 . 所以我们现在有两个变量,全局为 xe3y ,每个变量都包含一个引用 e2 内部事物的过程?

4 . 如果我是正确的, xy 所持有的程序是否像 e2 的指针一样?

3 回答

  • 1

    在按值传递的情况下,将评估函数的参数,并将它们的值绑定到函数参数 .

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

    如果一个值很简单,就像一个整数,那个整数被“复制”在新环境中分配的一些内存单元格中,如果它更复杂,比如一个闭包(一个编译过的函数),你可以认为“引用“将该对象复制到新环境中 .

  • 2

    传递值并不意味着它不是对象的地址,因为它经常是 . C是一种按值传递的语言 . 以下是pass by value的一个属性的示例:

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

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

    现在在 testx 的值已经改变,所以如果你有其他入口点用 x 做其他东西,那么它将作为一个对象 . 这就是 make-withdraw 的工作原理 .

    (define x (make-withdraw 100))
    

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

    (f x)
    

    这使得一个环境保持 x ,这是关闭其环境的机制,并且绑定 x 在调用结束时解析 . 它不会复制 x ,但是当 f 启动时,它永远不会依赖 x ,因为它传递了值,而不是名称 . 因此 (f x) 与始终调用 (x 25) 相同 . 没变!

  • 1

    我发现考虑这个问题的最好方法是考虑绑定 .

    绑定

    绑定是名称和值之间的关联 . 该名称通常称为'variable',值是变量的值 . 绑定的值可以是语言可以谈论的任何对象 . 然而,绑定是幕后的事情(有时称为'not being first-class objects'):它们不是可以用语言表示的东西,而是可以用作语言工作模型的一部分的东西 . 所以绑定的值不能是绑定,因为绑定不是第一类的:语言不能谈论绑定 .

    关于绑定有一些规则:

    • 有创建它们的形式,其中最重要的两个是 lambdadefine ;

    • 绑定不是第一类 - 语言不能将绑定表示为值;

    • 绑定是或可能是可变的 - 您可以在绑定存在时更改绑定的值 - 并且执行此操作的表单是 set! ;

    • 没有破坏绑定的运算符;

    • 绑定具有词法范围 - 一些代码可用的绑定是您可以通过查看它看到的绑定,而不是您必须通过运行代码猜测的绑定,这可能取决于系统的动态状态;

    • 只能从给定的代码位访问给定名称的一个绑定 - 如果多个词汇在词汇上可见,那么最里面的一个会影响任何外部的;

    • 绑定具有无限范围 - 如果绑定可用于一些代码,则它始终可用 .

    显然,这些规则需要进行详细阐述(特别是关于全局绑定和前向引用绑定)和mare formal,但这些足以理解会发生什么 . 特别是我不认为你需要花费大量时间来担心环境:一些代码的环境只是它可以访问的绑定集,所以不要担心环境只是担心绑定 .

    按值调用

    那么,'call by value'的意思是当你用一个参数调用一个过程时传递给它的变量(绑定)是变量绑定的值,而不是绑定本身 . 然后,该过程将创建具有相同值的新绑定 . 接下来是两件事:

    • 原始绑定不能被程序改变 - 这是因为程序只有它的值,而不是绑定本身,并且绑定不是第一类,所以你不能通过将绑定本身作为值;

    • 如果值本身是一个可变对象(数组和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 的值是一个程序,当被调用时:

    • 创建一个名为 x 的新绑定,其值为 silly 的参数;

    • 变异此绑定,因此其值增加1;

    • 返回此绑定的值,该值比调用它的值多一个 .

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

    • 创建了两个绑定,一个名为 fn ,一个名为 val ;

    • 使用 val 绑定的值调用 fn 绑定的值;

    • 返回 val 绑定的值 .

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

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

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

    这里重要的是,在 inner 中, x 有两个可能在词汇上可见的绑定,但最接近的一个 - 由 inner Build 的绑定 - 获胜,因为只有一个给定名称的绑定可以在一度 .

    以下是使用显式 lambda s表示的前一个代码(如果 inner 是递归的,这将不等效):

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

    最后一个变异绑定的例子:

    (define (make-counter val)
      (λ ()
        (let ((current val))
          (set! init (+ val 1))
          current)))
    
    > (define counter (make-counter 0))
    > (counter)
    0
    > (counter)
    1
    > (counter)
    2
    

    所以,这里, make-counter (是一个绑定的名称,其值是一个过程,当被调用时)为 val Build 一个新的绑定,然后返回它创建的过程 . 此过程生成一个名为 current 的新绑定,它捕获 val 的当前值,改变 val 的绑定以向其添加一个绑定,并返回 current 的值 . 此代码执行'if you can ever see a binding, you can always see it'规则:只要该过程存在,由 make-counter 调用创建的 val 绑定对于它返回的过程是可见的(并且该过程至少在存在绑定的情况下存在),并且它还会改变与 set! 的绑定 .

    Shorthands

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

    • ' x 是一个程序,... ' means ' x 是绑定的名称,其值是一个程序,当被调用时,......';

    • ' y 是...... ' means ' y 是绑定的名称,其值为......';

    • ' xy ' means '调用 x 命名的绑定值用 y '命名的绑定值调用;

    • '...将 x 绑定到... ' means ' ...创建一个名为 x 的绑定,其值为......';

    • ' x ' means ' x '的值;

    • 等等 .

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

    答案

    最后,在这个长序言之后,这是你问的问题的答案 .

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

    make-withdrawbalance 绑定到其参数并返回它生成的过程 . 这个程序,当被调用时:

    • amount 与其论点联系起来;

    • amountbalance 进行比较(它仍然可以看到,因为它可以在创建时看到它);

    • 如果有足够的钱,那么它会改变 balance 绑定,将其值减去 amount 绑定的值,并返回新值;

    • 如果没有足够的钱,它会返回 "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"
    

    注意:

    • 在此过程中的任何位置都不会复制第一类对象:没有创建过程的'copy';

    • 在此过程中,任何地方都没有突变第一类对象;
      在此过程中创建

    • 绑定(后来变得无法使用,因此可以被销毁);

    • 在此过程中重复突变一个绑定(每次调用一次);

    • 我没有必要提到'environments',这只是代码中某一点可见的绑定集,我认为这不是一个非常有用的概念 .

    我希望这有一点道理 .


    上述代码的更精细版本

    您可能希望能够执行的操作是撤销帐户中的交易 . 一种方法是返回一个撤销最后一笔交易的程序,以及新的余额 . 这是一个程序,它是这样做的:

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

    当你使用这个程序创建一个帐户,然后调用它会返回一个缺点:汽车是新的余额,或者字符串 "Insufficient funds" ,其中的cdr是一个撤销你刚才做的事务的过程 . 请注意,它通过显式放回旧余额来撤消它,因为我认为在浮点运算的情况下你不一定依赖 (= (- (+ x y) y) x) 是真的 . 如果您了解其工作原理,那么您可能了解绑定 .

相关问题