2.2.5 计算 (Computation bycalculation)
[
Computation,calculation,都翻成了计算,大概强调的重点不同吧。
Computation by calculation,也还是翻成计算吧。
evaluate,多数时候也翻译成计算,有时也译成分析。
]
我们在前两节中讨论了思考有关程序执行的新方法。要了解命令式程序的执行,就必须理解是如何改变状态的。使用面向对象的命令式语言编写程序,状态不仅包括所有对象的内部状态,而且包括当前正在执行的语句(在每个线程中),以及在每个堆栈中所有局部变量的状态。当前正在执行的语句是状态的一部分,了解这一点非常重要,因当你在纸上写程序执行时,很难跟踪状态。
在函数编程中,我们可以使用一种叫“计算的计算”的方法(即,computation by calculation,计算所调用的计算方法,Computation,似乎强调计算的结果,calculation是计算的方法,亦或相反。以下就简称计算)。这种方法对于 Haskell 来说特别重要(参补充材料“Haskell 中的数学纯”),Haskell 在《The Haskell School of Expression》[Hudak, 2000] 中有更详细地讲述。使用计算,我们从原始的表达表开始(比如函数调用),执行一步(比如,用函数体替换调用,或者计算基本数学运算的结果)。就这样多次重复,我们可以很轻松地分析出程序是如何计算的。
如果我们想要也解函数在边界上的行为,这种方法特别有用。在清单 2.2 中,我们用它来分析 SumNumber 的行为,把区间的上、下边界取相同的数。
我们先分析调用 SumNumbers(5, 5):
清单 2.2 用函数方式分析表达式 SumNumbers(5,5)
SumNumbers(5, 5)
用函数体去展开函数调用,把函数中所有的参数用指定值替换(from = 5, to = 5):
(5 > 5) ? 0 : {
varsumRest = SumNumbers(5 + 1, 5);
5 +sumRest; };
化简条件运算表达式。首先,分析条件(5 > 5),然后,继续分析条件为假的分支:
var sumRest = SumNumbers(5 + 1, 5);
5 + sumRest;
计算赋给变量 sumRest 的值。这时,我们要展开 SumNumbers(6, 5),计算函数调用参数的值:
var sumRest =
return(6 > 5) ? 0 : {
varsumRest = SumNumbers(6 + 1, 5);
6+ sumRest; };
5 + sumRest
继续计算 sumRest 的值。分析条件 (6 > 5),用 then 分支的子表达式替换初始表达式:
var sumRest = 0
5 + sumRest
计算出变量的值以后,就用实际值去替换在表达式中所有出现这个变量的位置:
5 + 0
计算基本的加法运行符(+)的调用:
5
正如你所看到的,用这种方法写出函数代码的计算过程,是很容易的。虽然函数程序员不会真的花时间写出程序的运行过程,但是,了解计算的过程,还是有用的,因为这种思考函数代码的方式是很强大的。
当然,由于这个示例很简单,我们没有讨论很多重要细节。但请放心,我们会在下一章涉及到所有这些问题。清单 2.2 展示了计算另一个有意义的方面,是决定下一步应该分析表达式的哪一部分。在这个示例中,我们使用了最里面的子表达式,因此,我们分析了函数调用的所有参数,以及运算符的使用(有了条件运算符的意外,这会以不同方式处理)。这一策略称为,严格或热情(strict or eager),许多函数语言都使用这种策略,包括 F#,它类似于一句一句地执行代码。
Haskell 中的数学纯
Haskell 出现于 1990 年,已在学术界流行。在这一节,我们已经看到,在函数语言中,我们使用不可变的数据结构和不可变的值,而不用可变的变量。F# 没有严格遵守,因为仍然可以声明可变的值。这种非严格的方法对于 .NET 的互操作特别有用,因为大多数的 .NET 库依赖于可变的状态,它是为命令式的、面向对象语言,如 C# 和 VB.NET 而设计的。
相反,Haskell 严格执行数学纯。这样,在程序执行的顺序上,可以很灵活。在前面的示例中,我们提到 F# 首先计算表达式的最内层部分。而在 Haskell 中,由于没有副作用,所以计算的顺序(可能)不重要。由于重新排序没有相互依赖的代码,也就不会改变程序的含义。因此,Haskell 使用一种称为延迟计算(lazy evaluation)的技术,直到实际需要时(例如,输出到控制台),才会计算表达式的结果。
这种改变程序而不改变其含义的能力,在 F# 中也非常重要,我们将在第十一章学习如何用它来重构 F# 程序,将看到延迟计算也可以用在 F# 中,这是一种很有价值的优化方案。
在后面几节,我们讨论了程序状态,用递归写计算。我们敢保证,你一定想学习如何以可重用的方式,编写代码中较难的部分,这就是我们的下一节的主题。