我发现考虑这一点的最佳方式是考虑绑定,而不是环境或框架,它们只是绑定的容器。
绑定
绑定是 name 和 value 之间的关联。该名称通常称为“变量”,其值就是变量的值。绑定的值可以是语言可以谈论的任何对象。然而,绑定是幕后的东西(有时这被称为“不是一流的对象”):它们不是可以用语言表示的东西,而是可以用作模型一部分的东西语言是如何工作的。所以绑定的值不能是绑定,因为绑定不是一等的:语言不能谈论绑定。
有一些关于绑定的规则:
- 有创建它们的表单,其中最重要的两个是
lambda 和define;
- 绑定不是一流的——该语言不能将绑定表示为值;
- 绑定是,或者可能是,可变的——一旦绑定存在,你就可以改变它的值——这样做的形式是
set!;
- 没有破坏绑定的操作符;
- 绑定具有词法作用域 -- 一些代码可用的绑定是您可以通过查看它看到的绑定,而不是您必须通过运行代码来猜测的绑定,这可能取决于系统的动态状态;
- 对于给定名称,只有一个绑定可以从给定的代码中访问 - 如果不止一个在词法上可见,则最里面的绑定会遮盖任何外部的;
- 绑定有无限的范围 -- 如果绑定可用于某段代码,则它始终可用。
显然,这些规则需要详细阐述(尤其是关于全局绑定和前向引用绑定)并且是正式的,但这些足以理解会发生什么。特别是我不认为你需要花很多时间担心环境:一段代码的环境只是它可以访问的绑定集,所以不用担心环境,只需担心绑定。
按值调用
所以,“按值调用”的意思是,当您调用带有变量(绑定)参数的过程时,传递给它的是变量绑定的 值,不是绑定本身。然后该过程创建一个具有相同值的 new 绑定。由此得出两件事:
- 原始绑定不能被过程更改 -- 这是因为过程只有它的值,而不是绑定本身,并且绑定不是一流的,因此您不能通过传递绑定本身来作弊作为值;
- 如果值本身是一个可变对象(数组和 conses 是通常可变对象的示例,数字是不可变对象的示例),则该过程可以改变该对象。
绑定规则示例
所以,这里有一些这些规则的例子。
(define (silly x)
(set! x (+ x 1))
x)
(define (call-something fn val)
(fn val)
val))
> (call-something silly 10)
10
因此,我们在这里为silly 和call-something 创建了两个顶级绑定,它们的值都是过程。 silly 的值是一个过程,当被调用时:
- 创建一个新绑定,其名称为
x,其值为silly 的参数;
- 改变这个绑定,使其值加一;
- 返回此绑定的值,该值比调用它时使用的值大一。
call-something 的值是一个过程,当被调用时:
- 创建两个绑定,一个名为
fn,一个名为val;
- 用
val绑定的值调用fn绑定的值;
- 返回
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! 的绑定。
为什么不是环境?
SICP 在chapter 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-withdraw 将balance 绑定到它的参数并返回它创建的过程。这个过程,当被调用时:
- 将
amount 绑定到它的参数;
- 将
amount 与balance 进行比较(它仍然可以看到,因为它在创建时可以看到);
- 如果有足够的钱,它会改变
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 被调用,x,x 被(绑定到)上面构造的过程。在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) 为真。如果您了解其工作原理,那么您可能了解绑定。