请考虑从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 . 所以我们现在有两个变量,全局为 x
, e3
为 y
,每个变量都包含一个引用 e2
内部事物的过程?
4 . 如果我是正确的, x
和 y
所持有的程序是否像 e2
的指针一样?
3 回答
在按值传递的情况下,将评估函数的参数,并将它们的值绑定到函数参数 .
因此,如果参数是表达式,则对其进行求值,并将值绑定到参数 . 如果它是绑定到值的标识符,则该值绑定到参数 .
如果一个值很简单,就像一个整数,那个整数被“复制”在新环境中分配的一些内存单元格中,如果它更复杂,比如一个闭包(一个编译过的函数),你可以认为“引用“将该对象复制到新环境中 .
传递值并不意味着它不是对象的地址,因为它经常是 . C是一种按值传递的语言 . 以下是pass by value的一个属性的示例:
上面的代码永远不会改变
y
,因为x
是一个新的绑定,恰好指向与y
相同的值,(set! x 10)
使x
指向不同的值 .y
仍将指向原始值20
.现在在
test
中x
的值已经改变,所以如果你有其他入口点用x
做其他东西,那么它将作为一个对象 . 这就是make-withdraw
的工作原理 .上面的代码返回一个闭包,它的顶级环境
balanced
绑定到100
,当用一个金额调用它将set!
从它的clousurebalance
然后返回新值,除非资金耗尽 .这使得一个环境保持
x
,这是关闭其环境的机制,并且绑定x
在调用结束时解析 . 它不会复制x
,但是当f
启动时,它永远不会依赖x
,因为它传递了值,而不是名称 . 因此(f x)
与始终调用(x 25)
相同 . 没变!我发现考虑这个问题的最好方法是考虑绑定 .
绑定
绑定是名称和值之间的关联 . 该名称通常称为'variable',值是变量的值 . 绑定的值可以是语言可以谈论的任何对象 . 然而,绑定是幕后的事情(有时称为'not being first-class objects'):它们不是可以用语言表示的东西,而是可以用作语言工作模型的一部分的东西 . 所以绑定的值不能是绑定,因为绑定不是第一类的:语言不能谈论绑定 .
关于绑定有一些规则:
有创建它们的形式,其中最重要的两个是
lambda
和define
;绑定不是第一类 - 语言不能将绑定表示为值;
绑定是或可能是可变的 - 您可以在绑定存在时更改绑定的值 - 并且执行此操作的表单是
set!
;没有破坏绑定的运算符;
绑定具有词法范围 - 一些代码可用的绑定是您可以通过查看它看到的绑定,而不是您必须通过运行代码猜测的绑定,这可能取决于系统的动态状态;
只能从给定的代码位访问给定名称的一个绑定 - 如果多个词汇在词汇上可见,那么最里面的一个会影响任何外部的;
绑定具有无限范围 - 如果绑定可用于一些代码,则它始终可用 .
显然,这些规则需要进行详细阐述(特别是关于全局绑定和前向引用绑定)和mare formal,但这些足以理解会发生什么 . 特别是我不认为你需要花费大量时间来担心环境:一些代码的环境只是它可以访问的绑定集,所以不要担心环境只是担心绑定 .
按值调用
那么,'call by value'的意思是当你用一个参数调用一个过程时传递给它的变量(绑定)是变量绑定的值,而不是绑定本身 . 然后,该过程将创建具有相同值的新绑定 . 接下来是两件事:
原始绑定不能被程序改变 - 这是因为程序只有它的值,而不是绑定本身,并且绑定不是第一类,所以你不能通过将绑定本身作为值;
如果值本身是一个可变对象(数组和conses是通常可变的对象的示例,数字是不是对象的示例),则该过程可以改变该对象 .
有关绑定的规则示例
所以,这里有一些这些规则的例子 .
所以,这里我们为
silly
和call-something
创建了两个顶级绑定,这两个绑定的值都是过程 .silly
的值是一个程序,当被调用时:创建一个名为
x
的新绑定,其值为silly
的参数;变异此绑定,因此其值增加1;
返回此绑定的值,该值比调用它的值多一个 .
call-something
的值是一个程序,当被调用时:创建了两个绑定,一个名为
fn
,一个名为val
;使用
val
绑定的值调用fn
绑定的值;返回
val
绑定的值 .请注意,无论对
fn
的调用是什么,它都不能改变val
的绑定,因为它无法访问它 . 所以你可以知道,通过查看call-something
的定义,如果它完全返回(如果对fn
的调用没有返回它可能不会返回),它将返回其第二个参数的值 . 这种保证是'call by value'的意思:支持其他呼叫机制的语言(如Fortran)不能总是保证这一点 .这里有四个绑定:
outer
是一个顶级绑定,其值是一个过程,当它被调用时,为x
创建一个绑定,其值为其参数 . 然后它创建另一个名为inner
的绑定,其值是另一个过程,当它被调用时,为其参数创建x
的新绑定,然后返回该绑定的值加1 .outer
然后使用x
的绑定值调用此内部过程 .这里重要的是,在
inner
中,x
有两个可能在词汇上可见的绑定,但最接近的一个 - 由inner
Build 的绑定 - 获胜,因为只有一个给定名称的绑定可以在一度 .以下是使用显式
lambda
s表示的前一个代码(如果inner
是递归的,这将不等效):最后一个变异绑定的例子:
所以,这里,
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
是绑定的名称,其值为......';'
x
用y
' means '调用x
命名的绑定值用y
'命名的绑定值调用;'...将
x
绑定到... ' means ' ...创建一个名为x
的绑定,其值为......';'
x
' means 'x
'的值;等等 .
描述这样的绑定是很常见的,因为完全显式的方式只是痛苦的:我已经尝试(但可能在某些地方失败)在上面完全明确 .
答案
最后,在这个长序言之后,这是你问的问题的答案 .
make-withdraw
将balance
绑定到其参数并返回它生成的过程 . 这个程序,当被调用时:将
amount
与其论点联系起来;将
amount
与balance
进行比较(它仍然可以看到,因为它可以在创建时看到它);如果有足够的钱,那么它会改变
balance
绑定,将其值减去amount
绑定的值,并返回新值;如果没有足够的钱,它会返回
"Insuficient funds"
(但不会改变balance
绑定,所以你可以用较小的数量再试一次:真正的银行可能会从balance
绑定中掏出一些钱作为罚款) .现在
为
x
创建一个绑定,其值是上述过程之一:在该过程中balance
最初是100
.f
是一个过程(是一个绑定的名称,其值是一个过程,当被调用时)将y
绑定到其参数,然后使用25
参数调用它 .因此,
f
被调用x
,x
是(绑定到)上面构造的过程 . 在f
中,y
绑定到此过程(不是它的副本,而是绑定到它),然后使用25
参数调用此过程 . 此过程的行为如上所述,结果如下:注意:
在此过程中的任何位置都不会复制第一类对象:没有创建过程的'copy';
在此过程中,任何地方都没有突变第一类对象;
在此过程中创建
绑定(后来变得无法使用,因此可以被销毁);
在此过程中重复突变一个绑定(每次调用一次);
我没有必要提到'environments',这只是代码中某一点可见的绑定集,我认为这不是一个非常有用的概念 .
我希望这有一点道理 .
上述代码的更精细版本
您可能希望能够执行的操作是撤销帐户中的交易 . 一种方法是返回一个撤销最后一笔交易的程序,以及新的余额 . 这是一个程序,它是这样做的:
当你使用这个程序创建一个帐户,然后调用它会返回一个缺点:汽车是新的余额,或者字符串
"Insufficient funds"
,其中的cdr是一个撤销你刚才做的事务的过程 . 请注意,它通过显式放回旧余额来撤消它,因为我认为在浮点运算的情况下你不一定依赖(= (- (+ x y) y) x)
是真的 . 如果您了解其工作原理,那么您可能了解绑定 .