3.1.1 当前状态的变量

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。

猜你喜欢

转载自blog.csdn.net/gggwfn1982/article/details/81542340