3.1.1 当前状态的变量
为了演示有一个时变状态的计算对象的含义,让我们对从一个银行账户取款的情况
来做模型。我们用一个程序withdraw 实现这个操作,这个程序需要一个取款金额的
实际参数。如果账户中的金额比取款金额大,那么取款程序将返回取款后的余额。否
则,程序返回余额不足的消息。例如,如果我们开始操作时,账户中有余额100元,
我们在取款中将得到如下的返回序列。
(withdraw 25)
75
(withdraw 25)
50
(withdraw 60)
"余额不足"
(withdraw 15)
35
值得注意的是表达式(withdraw 25),解释两次,返回了不同的值。
这是程序的一种新的行为。直到现在,我们的所有的程序能被视为
计算数学函数的规范。一个程序的调用计算函数应用到给定的参数上
的返回值,对相同的程序的两次调用,以相同的参数,总是生成相同的结果值。
为了实现withdraw,我们能使用一个变量blance来显示账户中的钱的余额。
定义withdraw为一个程序,它读取变量blance。withdraw程序检查余额是不是大于
请求的数额。如果是,程序以余额减请求数,并返回新的余额值。否则程序返回余额不足的
消息。这是余额和取款程序的定义:
(define balance 100)
(define (withdraw amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
balance)
"Insufficient funds")
)
通过如下的表达式完成,减balance
(set! balance (- balance amount))
这使用了set!这个关键字,它的语法是
set! <name> <new-value>)
这<name>是一个符号,<new-value>是任何的表达式.
set!为<name>赋值为<new-value>这个表达式的解释后的结果值.
在手上的例子,我们把balance修改为前一个balance减去amount的新值.
如果条件测试通过,withdraw程序使用begin这个关键字来引出两个
表达式的解释.第一个表达式计算balance的减法,然后返回balance.
总之,解释表达式如下:
(begin <exp1> <exp2>...<expk>)
引出表达式从<exp1>到<expk>按顺序进行解释,返回最后一个表达式的解释
的结果值,作为整个begin的语句块的返回值。
尽管withdraw的工作是正常的,变量balance显示出一个问题.正如上面所指定的,
balance是在全局环境中定义的一个名称,并且能被任何程序自由地存取和修改.
如果我们能把balance放在withdraw的内部,就更好了.那样的话,仅有withdraw程序能
直接读取balance,其它的程序仅能通过调用withdraw程序的方式来间接地读取balance.
这是更准确的模型,它的观念是balance是一个局部的状态变量,由withdraw程序来使用,
以跟踪账户的状态。
通过如下的方式重定义withdraw程序,我们能让balance成为内部的变量。
(define new-withdraw
(let ((balance 100))
(lambda (amount)
(if (>= balance amount)
(begin (set! balance (- balance amount)) balance)
"Insufficient funds"))
)
)
这里我们已经做的事是使用let来建立一个有局部变量balance的环境,并且绑定初始化的值
100。有了这个局部的环境,我们使用lambda表达式来创建一个程序,以amount为参数,
行为像我们之前的取款程序。这个程序返回let表达式的解释的结果。new-withdraw与
之前的withdraw在行为上高度一致,只有它的变量balance是局部的,不能被其它程序直接读取。
组合set!和局部变量是组装有局部状态的计算对象而使用的通用的编程技术。
不幸的是,使用这种技术引起了一个严重的问题:当我们开始介绍程序时,我们也在
1.1.5部分中介绍了一个程序解释时的替换模型来提供程序的含义的解释。我们说应用一个程序
应该被解释为解释它的程序体,并且用它的实际值来替换它的形式参数.麻烦在于,
只要我们引入了赋值语句,对于程序,替换就不再是一个能用的模型了。
(在3.1.3部分中我们能看到这是为什么。)作为一个结果,我们在此时在技术上,没有方式理解
为什么new-withdraw程序能有如上要求的行为。为了能够真正地理解如new-withdraw这样的程序
我们需要开发一个新的程序解释模型。在3.2部分中我们将介绍这样的模型,结合set!和
局部变量的解释。首先,然而我们检查new-withdraw确立的模式的一些变化。
如下的程序make-withdraw,创建了取款的处理过程。make-withdraw形式参数balance
指定了账户中的初始的钱数。
(define (make-withdraw balance)
(lambda (amount)
(if (>= balance amount)
(begin (set! balance (- balance amount)) balance)
"Insufficient funds"
)
)
)
make-withdraw程序能被用来创建如下的两个对象 W1和 W2:
(define W1 (make-withdraw 100))
(define W2 (make-withdraw 100))
(W1 50)
50
(W2 70)
30
(W2 40)
"Insufficient funds"
(W1 40)
10
注意的是W1和W2是完全不同的两个对象,任何一个对象都有它自己的局部状态变量
balance.从一个取款,不影响其它的。
我们也能创建对象,像处理取款一样来处理存款,因此我们能表示一个简单的银行账户。
这是一个程序,它返回一个银行账户的对象,并且有一个指定的初始余额:
(define (make-account balance)
(define (withdraw amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
balance)
"Insulfficient funds"))
(define (deposit amount)
(set! balance (+ balance amount))
balance)
(define (dispatch m)
(cond ((eq? m 'withdraw) withdraw)
((eq? m 'deposit) deposit)
(else (error "Unknown request -- MAKE ACCOUNT" m))
))
dispatch)
对make-account的任何一次调用,都安装了一个有局部状态变量balance的环境.
有了这个环境, make-account定义了程序deposit和withdraw来读取balance和
一个附加的程序dispatch.这个程序带一个消息作为它的输入值,返回两个内部
程序中的一个.这个程序dispatch本身是作为返回值,这个值表示了银行账户的对象.
这正是我们在2.4.3部分中看到的编程的消息传递风格,尽管这里我们使用它
来注入修改局部变量的能力.
Make-account能被如下的方式使用:
(define acc (make-account 100))
((acc 'withdraw) 50)
50
((acc 'withdraw) 60)
"Insufficient funds"
((acc 'deposit) 40)
90
((acc 'withdraw) 60)
30
对acc的任何一次调用都返回局部定义的程序deposit或者是withdraw,
它们都应用到特定的amount. 正如make-withdraw的例子,对make-account
的另一个调用:
(define acc2 (make-account 100))
将生成一个完全独立的账户对象,它维护着自己的局部变量balance.
练习3.1
一个累加器是一个程序,它重复地调用一个单独的数字参数,累加它的参数到一个和.
任何一次它被调用,它能都返回当前的和的值.
写一个程序make-accumulator来生成一个累加器,任何一个累加器都维护着一个独立的和.
make-accumulator的输入值应该指定一个和的初始值;例如:
(define A (make-accumulator 5))
(A 10)
15
(A 10)
25
练习3.2
在软件的测试应用中,在计算的过程中,能够对一个给定的程序的被调用的次数进行计数
是很有用的.写一个程序make-monitored以一个程序f为输入参数,这个f有一个参数。
由make-monitored返回的结果是一个程序,叫mf,它通过维护一个内部的计数器,来跟踪
它已经被调用的次数。如果mf的输入是特定的符号how-many-calls?,
那么mf返回调用的次数。如果输入是 特定的符号reset-count, mf将计数器为0。
对于任何其它的输入,mf返回以输入值为参数,调用f的结果并且对计数器加一。
例如,我们能为sqrt程序写一个监控版本的。
(define s (make-monitored sqrt))
(s 100)
10
(s 'how-many-calls?)
1
练习3.3
修改make-account程序来创建有密码保护的账户。
也就是说,make-account应该有一个符号作为附加的输入参数.如下所示:
(define acc (make-account 100 'secret-password))
这个账户对象应该仅能处理这样的请求,即账户的创建伴随着密码,其它的情况
应该返回一个报错的信息。
((acc 'secrett-password 'withdraw) 40)
60
((acc 'some-other-password 'deposit) 50)
"Incorrec password"
练习3.4
修改练习3.3中的程序make-account,加上另一个局部状态变量,
如果一个账户接收到了超过连续的七次错误的密码,它就调用程序call-the-cops。