条件表达式和函数
对于许多问题,计算机程序必须以不同的方式处理不同的可能情况。如,一个游戏程序必须确定一个物体的移动速度是否在某个范围之内或确定一个物体是否位于屏幕某个特定的区域。而对于一个控制电机运行的程序,则可能需要描述描述阀门什么情况下应被打开。为了处理这些情况,我们必须使用一种方式来阐述所条件为真或为假,即需要一种新的数据类型。通常,该类型的值被称为布尔值(或真值)。本节介绍布尔类型,计算为布尔值的表达式以及依赖于布尔值进行计算的表达式。
布尔类型和关系
考虑一下问题:
XYZ公司给雇员的报酬是每小时12美元。通常一个员工每周工作20到65小时。如果一个雇员每周工作时数在上述范围内,试编写程序确定其周工资。
斜体部分表示了程序必须以某种方式处理它的输入,如果输入在给定范围内,按常规进行,否则,则需考虑其他的计算公司。简而言之,就像人们对不同的情况分别进行推理一样,程序使用条件表达式对不同的情况进行计算。
条件并不是一种新的概念,数学所说的断言的真假就是一种条件。例如,一个数可能等于,小于或大于另一个数。因此如果x和y是数,则关于x和y的关系如下:
- x = y:x等于y;
- x < y: x严格小于y;
- x > y: x严格大于y。
对于任何一对给定的(实)数,上述断言有其只有一个是正确的。一般地,一个断言对于变量的某些取值为真,其余取值为假。
除了确定一个基本断言在给定的情况下是否为真以外,有时确定断言的组合是否为真也很重要。考虑以下3个断言,它们可以以下面的方式组合在一起:
- x = y且x < y 且 x > y;
- x = y或x < y 或 x > y;
- x = y或x < y
第1个复合断言是假的,因为 不管x和y的值为何,3个断言中有2个必定为假,因此其组合也为假;另外,不管x和y取何值,第2个复合断言为真;最后,第3个复合断言,在一些情况下为真,在另一些情况下为假。
与数学语言一样,Scheme中有自己表示真假的词汇,有陈述基本断言的词汇,有将基本断言组合为复合断言的词汇。在Scheme中,真表示为true,假表示false。如果断言涉及两个数的关系,通常会用关系操作,如=,< 和 > 等。
上述三个数学断言的转换遵循大家所熟悉的先写括号,再写操作符,然后是参数,最后是右括号这样的Scheme表达式结构:
(= x y): x等于y;
(< x y): x严格小于y;
(> x y): x严格大于y。
以后大家还会遇到诸如<=
和>=
这样的关系操作符。
比较两个数值大小的Scheme表达式和其他Scheme表达式一样有一个结果,但其结果不是数值,而是true和false。当关于两个数的Scheme断言为真时,值为true,如
(< 4 5)
= true
断言为假时,值为false,如
(= 4 5)
= false
复合条件在Scheme中的表示也很自然。如果要把(= x y)
和(< x y)
组合在一起,表示当两个条件为真时复合断言为真,可以写成:
(and (= x y) (< y z))
类似地,如果想表示至少两个条件之一为真时,复合断言为真。
(or (= x y) (< y z))
最后,下述表达式。
(not (= x y))
表示断言的否定为真。
下面一些章节将解释为何程序设计需要明确地对条件进行陈述和推理。
函数和条件测试
下面是一个简单的对变量取值进行测试的函数:
;; is-5? : number -> boolean
;; 确定n是否等于5
(define (is-5? n)
(= n 5))
该函数仅当输入为5时,值为true。在函数的合约中包含了一个新的要素boolean。与number一样boolean是Scheme内建的一种数据类型。不同的是,boolean仅包含两个值,true和false。
下面是一个稍微有点意思的输出类型为boolean的函数:
;; is-boolean-5-6? : number -> boolean
;; 确定n的值是否位于5和6之间(不包括5和6)
(define (is-between-5-6? n)
(and (< 5 n) (< n 6)))
如果输入的值介于5和6之间(不包括5和6),函数输出结果为true。理解此类函数的一种较好的方式是认为函数划分了数轴上的一个区间:
区间边界:一个以“(”或“)”标识的区间是不包含边界的,而以“[”或“]”标识的区间则包含边界。
下述函数划了一个较为复杂的区间:
;; is-between-5-6-or-over-10? : number -> boolean
;; 确定n是否介于5和6之间(不包括5和6)或者大于等于10
(define (is-between-5-6-or-over-10? n)
(or (is-between-5-6? n) (>= n 10)))
对于数轴上两个区间内的任何数值,函数返回true:
左边区间是5和6之间,但不包括5和6的任何数值,右边是从10开始且包括10的无限区间,数轴上两个区间的点都满足函数is-between-5-6-or-over-10中的条件表达式。
上述3个函数对数值进行了条件测试。为了设计或理解此类函数,必须理解区间和它们的组合(也称为区间的并)。
条件和条件函数
一些银行对于不同的存款给予不同的利润,客户存的越多,银行给的利率就越高。在这种情况下,利率依赖于存款额。为了帮助银行职员,银行使用了一个利率计算函数,根据客户的存款额,函数将给出相应的利率。
利率计算函数必须确定对于给定的输入,哪个条件为真,因此说利率计算函数是一个条件函数。我们可以使用条件表达式来定义这些利率计算函数,条件表达式的一般形式为:
(cond (cond
[queston answer] [queston answer]
... or ...
[queston answer] [else answer]
其中省略号表示一个cond表达式可包含任意数目的cond行。每一cond行,也称为cond子句,它包含两个表达式,分别称为条件表达式(condition)和答案表达式(answer)。条件表达式是一个含有参数的布尔表达式,而答案表达式则是一个普通的Scheme表达式,若条件表达式为真,后者会根据参数的值来进行计算。
计算cond表达式时,Scheme先确定每个条件表达式的值,是true还是false。对于第一个条件为true的cond子句,Scheme执行其答案部分,答案部分的值就是整个cond表达式的值。以下两个简单的例子:
(cond (cond
[(<= n 1000) .040] [(<= n 1000) .040]
[(<= n 5000) .045] [(<= n 5000) .045]
[(<= n 10000) .055] [(<= n 10000) .055]
[(> n 10000) .060]) [else .060])
如果将n替换为20000,则两个例子的前3个条件的计算结果皆为false。而左边条件语句中的表达式(> 20000 10000)的计算结果为true,因此答案是为0.60;对于右边的条件语句,else子句给出了整个表达式的值,也是0.60.反之,如果n为10000,则值为.055,因为对于两个表达式来说,(<= 10000 1000)和(<= 10000 5000)的计算结果都为false,而(<= 10000 10000)的计算结果为true。
借助cond表达式,现在可以定义本节开始时提到的利率计算函数。假定存款额小于等于1000美元的银行利率为4%,大于1000美元,小于等于5000美元定为4.5%,大于5000美元定为5%。显然函数的输入为一个数值,而结果为另一个数值。
;; interest-rate : number -> number
;; 确定给定amount存款额的利率
(define interest-rate amount) ...)
而且,问题表述提供了3个例子:
(= (interest-rate 1000) .040)
(= (interest-rate 5000) .045)
(= (interest-rate 8000) .050)
注意,如果可能的话,例子将用布尔表达式表示。
函数主体应该是一个cond表达式,由它区分问题表述中所涉及的3种情况,以下是程序框架:
(cond
[(<= amount 1000) ...]
[(<= amount 5000) ...]
[(> amount 5000) ...]
使用例子和上述框架,容易给出如下定义:
(define (interest-rate amount)
(cond
[(<= amount 1000) 0.040]
[(<= amount 5000) 0.045]
[(> amount 5000) 0.050]))
由于仅需考虑3种情况,还可以将第3个条件用else代替:
(define (interest-rate amount)
(cond
[(<= amount 1000) 0.040]
[(<= amount 5000) 0.045]
[else 0.050]))
对于某顾客的存款额(如4000)当应用interest-rate
时,计算过程会如预料的那样进行。Scheme首先拷贝该函数主体,然后用4000代替amount:
(interest-rate 4000)
= (cond
[(<= 4000 1000) 0.040]
[(<= 4000 5000) 0.045]
[else 0.050]
= 0.045
因为第一个条件的值为false,而第2个条件的值为true,因此程序的结果为0.045或4.5%。如果使用(> amount 5000)
而不是使用else
,计算过程也是一样的。
条件函数的设计
与设计一般函数相比,条件函数的设计比较复杂,程序设计者必须了解问题表述中所列出的不同情况加以识别。为了强调这种思想的重要性,这里介绍并讨论条件函数的设计过程。该过程引入了一个新的设计诀窍数据分析(data analysis),它要求程序设计者理解问题表述中所涉及的不同情况。因此,有必要对2.5节中所讨论的程序设计诀窍中的例子和程序体部分进行一些修改。
数据分析和定义
了解了问题表述所涉及的不同情况后,必须确定它们的数据定义 data definiton
,下面对这个思想进行深入讨论。
对于数学函数,一种好的策略是画出数轴,然后针对不同的情况确定相应的区间。考虑 interest-rate
函数的合约:
;; interest-rate : number -> number
;; 确定相应于存款额 amount (大于等于零)的利率
(define (interest-rate amount) ...)
该函数的输入是一个非负数,程序对于3种不同的情况给出不同的答案:
对于处理布尔值的函数,cond表达式必须区分两种不同情况,即true和false。我们很快将遇到其他形式的数据,这些数据需要对更多不同的情况进行推理。
函数例子
选择的例子应能说明不同的情况。如果这些情况可以用数值区间来刻划,还应该考虑所有的边界。
对于interest-rate
函数,应该使用0,1000和5000作为例子。另外,也应该选择500,2000和7000作为例子来检查区间内部数值的计算。
主体-条件
函数主体包含的cond表达式的数目应该与不同情况的数目一样,该要求提示了以下程序框架:
(define (interest-rate amount)
(cond
[... ...]
[... ...]
[... ...]))
接着必须阐明与每种情况相关的条件,条件是关于函数参数的断言,可以使用Scheme关系表达式或自定义的函数来表示。
对我们的例子而言,数轴区间的转换结果是如下3个表达式:
(and (<= 0 amount) (<= amount 1000))
(and (< 1000 amount) (<= amount 5000))
(< 5000 amount)
将它们加进函数,其最终结果为:
(define (interest-rate amount)
(cond
[(and (<= 0 amount) (<= amount 1000)) ...]
[(and (< 1000 amount) (<= amount 5000)) ...]
[(> amount 5000) ...]))
此阶段,程序设计者应该检查以下所选择的条件是否对输入进行正确的区分,尤其是,如果一个输入值属于某一种特定情况并用cond子句进行表示,那么位于该子句前的所有条件的计算结果都应该为false,而该子句的条件的计算结果为true。
主体-答案
最后,要确定对于每一个cond子句,函数应产生什么结果。具体来说,就是对于cond表达式中的每一行,如果条件为真,相应的表达式结果应该是什么。
对于我们的例子,结果由问题表述规定,分别是4.0,4.5和5.0.对于更复杂的例子,必须按照第1个设计诀窍中的建议,对每个cond子句,确定一个表达式。
提示:如果cond的答案部分较为复杂,最后每次设计一个答案。假定条件的计算结果为true,使用参数,基本运算和其他函数编写相应的答案,然后将整个函数应用于使条件为true的输入,并对所设计的答案进行计算,此时其他答案可以保留为”…”.
简化
完成了条件表达式的定义并对其进行测试之后,有些程序设计者仍然希望检查以下表达式条件是否能简化。在我们的例子中,由于amount的值总是大于等于0,因此第一个条件表达式可以写成:
(<= amount 1000)
而且,cond表达式是顺序进行计算的。在对第2个表达式进行计算的时候,第1个表达式的计算结果肯定为false,即amount的值不会小于等于1000,这使得第2个条件的左边成分变得多余。经过进一步简化的interest-rate的程序框架为:
(define (interest-rate amount)
(cond
[(<= amount 1000) ...]
[(<= amount 5000) ...]
[(> amount 5000) ...]))
条件函数的主体的设计
阶段 | 目标 | 任务 |
---|---|---|
数据分析 | 确认函数所要处理的所有不同情况 | 检查问题表述理出不同的情况 - 枚举出所有可能的情况 |
例子 | 对于每种情况提供一个例子 | 对每种情况至少选择一个例子 - 对于区间或枚举值,例子必须包括边界 |
主体-条件 | 阐明一个条件表达式 | 写出cond表达式的框架,每种情况一个子句 -对于每种情况阐明一个条件; - 确认条件能将例子适当区分 |
主体-答案 | 对于每个cond子句阐明条件答案 | 分别处理每个cond表达式 - 假定条件为真,设计相应的Scheme表达式,即条件答案 |
上表总结了设计条件函数的一些建议,请与图2.2联系在一起阅读,并比较程序体的设计过程。请在设计一个条件函数之后再次阅读上表。