数、表达式和简单程序
计算机善于进行数值处理。一旦了解计算机如何进行数值计算,只要将常识转换为程序语言符号,我们就可以设计出简单的计算机程序了。尽管如此,编写简单的程序也是需要原则的,因此本章的最后将介绍最基本的程序设计诀窍。
数和算数运算
在Scheme中对数进行加减乘除运算
(+ 5 5) (+ -5 5) (+ 5 -5) (* 3 4) (/ 8 12)
所有表达式都使用了括号,其中操作在前,接着以空格隔开操作数。
与算数、代数公式类似,Scheme表达式也可以嵌套使用。
(* (+ 2 2) (/ (* (+ 3 5) (/ 30 10)) 2))
由于每个表达式的形式皆为:
(operation A ... B)
除了包括常见的简单数学运算符外,Scheme还提供了一整套数学运算函数,以下是5个例子。
- (sqrt A)计算√A
- (expt A B)计算A的B次方
- (remainder A B)计算整数A除以整数B的余数
- (log A)计算A的自然对数
- (sin A)计算弧度A的正弦值
如果怀疑一个基本运算是否存在或欲了解其使用方式,请使用简单例子在DrScheme上进行测试。
关于数
当涉及实数的时候,和其他程序语言一样,Scheme在精度上做了这种考虑。例如,2的平方根是一个实数而不是一个有理数,因此Scheme不得不使用非精确数来表示它。
(sqrt 2)
=#i1.4142135623731
其中#i
警告程序设计者,计算结果是真正数值的一个近似表示。
造成非精确的原因是用简化的方式表示2的平方根或如发现这样的数值。实际上这些数的十进制表示是无限长的(不含循环),而在一台计算机中,数的表示长度是有限的,因此只能表示这些数的一部分。如果将这些数表示为固定长度的有理数,其结果必然是非精确的。
为了集中精力学习与计算相关的重要概念而不是拘泥于这些细节,DrScheme会尽量将数据处理为精确数。DrScheme的交互窗口显示一个如1.25或22/7这样饿数值时,它就是一个对精确的有理数韩剧哦分数进行运算的结果。
仅当一个数的前缀为#i
时,它才是一个数的非精确表示。
变量和程序
在代数中,可以用含变量的表达式阐明两个数之间的关系。变量是一个未知数的占位符(placeholder)。
一般来说,一个包含变量的表达式可以被认为是一条从给定值计算另一个数值的规则。
程序也是一种规则,它不仅告诉我们也告诉计算机应如何从一些数据产生另一些数据。一个大型的程序可能包括多个以某种方式组合起来的小程序。因此程序设计者在编写程序的时候给它们的命名是非常重要的。对于计算圆满面积面积的程序,合适的名字是area-of-disk.
(define (area-of-disk r)
(* 3.14 (* r r)))
上两行程序指明area-of-disk是一条规则,r是唯一的输入,一旦知道了r的值,程序的结果或输出就是( 3.14 (* r r))*.
函数一旦被定义,此后就可以如同基本函数那样被使用。对函数各右边列出的变量,我们必须提供一个输入,也就是说,我们可以编写表达式,其中操作是area-of-disk, 其后跟着一个数值。
(area-of-disk 5)
其意为将area-of-disk应用于数值5。
很多程序的输入多于一个,计算圆环(中心有一个洞的圆盘)面积的程序就是一个例子:
我们知道圆盘的面积是外盘的面积减去内盘的面积,这意味该程序需要两个未知量:外盘的半径outer和内盘的半径inner,因此,计算圆环的面积的程序可以写成:
(define (area-of-ring outer inner)
(- (area-of-disk outer)
(area-of-disk inner)))
此3行代码表示area-of-ring是一个程序,该程序有两个输入,outer和inner,并且程序结果是(area-of-ring outer)和(area-of-disk inner)之差。换句话说,area-of-ring使用了Scheme的操作和前面已定义过的函数。
字处理问题
程序设计者一般较少处理诸如将数学公式转换为程序这样的问题,它们通常要处理的问题往往缺乏形式化的描述,包含不相关甚至含糊的信息。程序设计者的第一个任务就是从问题中提取相关信息然后用合适的表达式阐明。以下是一个典型例子:
XYZ公式所有雇员的报酬都是每小时12美元。通常每个雇员每周工作20到65小时,试编写一个程序按照雇员的每周工作时数计算其周薪。
最后一句话提出了实际的任务:编写一个程序根据某些数值计算另一个数值。具体来说,程序的输入是一个数值,即每周工作时数,输出是另一个数值,即周薪。第一句话说明计算是如何进行的,但没有对其明确阐明,在此,这不会引起问题。容易看出,如果一个雇员工作了h小时,他的周工资就是
12*h
知道了规则,用Scheme语句写出来就是:
(define (wage h)
(* 12 h))
该程序的名字为wage,参数是h,一个雇员的每周工作时数,结果为相应的周薪,即(* 12 h)
.
错误
编写Scheme程序必须遵循一些规则。这些规则在计算机能力和人们的行为之间进行折衷。
语法错误
按下DrScheme的Execute按钮后,Scheme程序设计环境首先会按照Scheme的语法规则检查程序定义是否合法。如果Definitions窗口中程序的某一部分不合法,DrScheme将提示相应的语法错误,给出相应的错误消息并高亮显示出现错误的地方。
运行错误
并不是所有合法的Scheme表达式都有结果。
当让一个合法的Scheme表达式进行除数为零的除法运算,或进行其他无意义的算数运算,或当一个程序的输入数目有错时,DrScheme将停止计算过程并给出运行错误消息。通常是在交互窗口中显示一个解释并高亮显示发生错误的表达式。高亮显示的表达式是引发错误的原因。
逻辑错误
一个良好的程序设计环境可以帮助程序设计者发现语法错误和运行错误。程序设计者也可能制造逻辑错误。一个逻辑错误可能不会激发任何错误消息,但计算所得的结果却是错误的。
只有细心和系统地设计程序,程序设计者才能捕获到此类错误。
设计程序
上面章节说明程序设计需要考虑许多步骤,需要确定问题描述中哪些信息是相关的,哪些问题是可以忽略的,需要了解程序的输入和输出以及它们之间的关系。我们必须知道或查明Scheme是否提供所需处理数据的基本操作,如果没有,还必须设计一些辅助函数来实现它们。最后,一旦编写程序,还必须验证,测试它是否能完成预期任务。该问题可能会暴露一些语法错误,运行问题甚至逻辑错误。
要解决这些表面上混乱的情况,必须建立并遵循一套设计诀窍,即规定完成任务的顺序以及每步如何进行。基于到目前位置所得到的经验,设计一个程序至少需要如下4各步骤。
理解程序的目的
程序设计的目标是创建一个接受输入并产生结果的机制。因此在开发程序时应该给每一个程序一个有意义的名字,并且说明输入数据和所产生的数据的类型,这称为程序的合约。下面是程序area-of-ring的合约:
;; area-of-ring : number number -> number
其中分号表示该行是一个注释。合约包含两个部分,冒号的左边是程序的名字,右边是输入和输出的类型,输入和输出之间用箭头隔开。
一旦有了合约,就可以在程序中加入函数头部,函数头部复述了程序的名字,同时给每个输入一个不同的名字。这些名字是(代数)变量,是程序的参数。
下面是程序area-of-ring的合约和函数头部:
;; area-of-ring : number number -> number
(define (area-of-disk outer inner) ...)
它们表示程序的第一个输入为outer, 第二个输入为inner.
最后,基于合约和参数,简要阐明一下程序的用途说明,它是程序要完成的任务的简短注释。对大多数程序,一到两行就足够了,更大的程序则需要更多的信息来说明其用途。
;; area-of-disk : number number -> number
;; 计算一个半径为outer,洞的半径为inner的圆环的面积
(define (area-of-ring outer inner) ...)
提示:如果问题表述包含数学公式,公式中不同变量的数目可能是程序的输入数。
为了将给定的事实与要计算的数据分开,我们必须仔细检查问题表述。如果给定的一个固定数值,它可能要在程序中出现。如果给定的是一个稍后需要确定的未知数,它就是一个输入,而问题表述中的询问(或要求)则提示了程序的名字。
例子
为了更好地了解程序要计算什么,需要构造一些输入并确定输出到底是什么。例如,对于输入5和3,程序area-of-ring的计算结果应为50.24,这是应为程序的输出是外圆盘的面积与内圆盘面积之差。
在用途说明中加入例子:
;; area-of-ring : number number -> number
;; 计算一个半径为outer,洞半径为inner的圆环的面积
;; 例子: (area-of-ring 5 3) 的结果为50.24
(define (area-of-ring outer inner) ...)
在编写程序体之前构造例子从多方面看来都是有益的。首先,它是唯一可靠的在程序测试中发现逻辑错误的途径。避免用最终构造的程序来构造例子,一面犯轻信程序的错误。第二,例子使我们思考数据计算过程,这对于将遇到的复杂程序体的设计是至关重要的。最后,例子是用途说明的非正式表达。此后的程序读者,如教室,同事还有程序购买者会喜欢这些抽象概念的具体说明。
程序体
最后必须阐明程序体,即必须将函数头部中的”…“替换为表达式。该表达式使用Scheme中的基本操作和已定义或即将定义的程序,由参数计算出结果。
只有理解了如何从给定的输入计算出结果,才可能阐明程序体。如果输入和输出的关系由数学公式给出,只要将数学公式转换为Scheme表达式即可。如果给定的是一个书面叙述的问题,我们必须细心地挖掘其中的信息并构造相应的表达式。最后,观察并理解如何从特定的输入得到输出的例子可能对程序体的设计也会有所帮助。
在我们说讨论饿的例子中,计算任务是一个非正式说明的公式,它使用了先前定义的程序area-of-disk, 下面是对它的Scheme翻译:
(define (area-of-ring outer inner)
(- (area-of-disk outer)
(area-of-disk inner))
测试
在完成了程序定义之后,还必须测试程序。至少应该确定对于给定的例子,程序计算所得得结果与预期数值是否相符。为了简化程序测试过程,通常可以在Definitions窗口得下面如同添加等式一样添加一些例子。然后,按下Execute按钮,计算它们,并观察对于这些例子程序是否正常工作。
测试不能保证程序对所有可能得输入都产生正确得输出,因为可能得输入数目通常是无限得。但测试可以揭示语法错误,运行问题以及逻辑错误。
对于错误的程序输出,必须特别关注程序例子。有可能例子本身就是错误的,也有可能程序包含了错误逻辑,也有可能例子和程序都有错误。不管是何种情况,都必须在此经历程序开发的每一步。
现在我们按照上述开发程序诀窍写一个完整的例子。
;; 合约: area-of-ring : number number -> number
;; 用途: 计算一个半径为outer, 其中洞的半径为inner的圆环的面积
;; 例子: (area-of-ring 5 3) 的计算结果为50.24
;; 定义: [函数头部的精化]
(define (area-of-ring outer inner)
(- (area-of-disk outer)
(area-of-disk inner)))
;; 测试:
(area-of-ring 5 3)
;; 预期的值
50.24
| 阶段 | 目标 | 任务 |
| —- | —- | —- |
| 合约,用途说明和函数头部 | 给函数命名:
指定输入和输出的类型;
描述函数的用途说明;
阐明函数头部 | 给函数起一个合适的名字
- 已函数需要的未知数为线索研究问题;
- 给每个输入起一个名字,如果可能的话,使用在问题描述中给定的名字;
- 使用选择的变量名描述函数应该产生什么结果;
- 阐明合约和函数头部:
;;name : number …->number
;; x1 开始计算..
(define (name x1…)…) |
| 例子 | 通过例子刻划输入和输出之间的关系 | 检查问题表述得到例子
- 计算例子;
- 如果可能的话,检查计算结果;
- 构造例子 |
| 主体 | 定义函数 | 阐明函数是如何计算它的结果的
- 使用Scheme基本运算,其他函数和变量构造Scheme表达式;
- 如果可以的话,翻译问题描述中的数学公式 |
| 测试 | 发现错误(拼写错误和逻辑错误) | 将函数应用于例子中的输入数据
- 检查结果与预期值是否相符 |
设计诀窍不是魔法,它并不能解决程序设计过程中所遇到的所有问题,它提供的是完成程序设计过程中必经步骤的指导。在程序设计中最富有创新性和最困难的一步是程序体的设计。它依赖于我们阅读和理解书面材料的能力,依赖于我们获取数学关系的能力,依赖于我们所掌握的基本事实。上述任何一点对计算机程序的开发者来说都不是特殊的,而所使用的知识对不同的应用领域来说却是有差异的。本书的其余部分将说明如何完成这最困难的一步。
领域知识
阐明程序体通常需要与问题相关的知识,这种形式的知识称为领域知识(domain knowledge)。它可能来自简单或复杂的数学,如算术和微分方程,或来自非数学学科,如音乐,生物学,土木工程和艺术等。
一个程序设计者不可能了解所有计算应用领域,但他必须准备去了解不同应用领域的语言,以便和领域专家沟通, 所使用的语言可能是数学。但有些情况下,程序设计者必须发明一种语言,特别是描述应用领域数据的语言。因此,程序设计者必须对计算机语言的所有可能性有充分的理解。