为什么要函数式编程
概述
当软件越来越复杂,结构化也越来越重要。结构良好的软件很容易写和调试,还能提供易于重用的模块来减轻将来的工作。这篇文章我们详细展示了函数式语言的两个特征——高级函数和惰性求值——对于模块化的重要意义。作为例子,我们使用列表和树编写几个算法,并实现 alpha-beta 剪枝(一种搜索算法,用以减少极小化极大算法搜索树的节点数)。结论是因为模块化是成功编程的关键,所以函数式编程为软件开发提供了重要的优势。
简介
本文试图向更大的(非函数式)程序员群体展示函数式编程的意义,同时也帮助函数式程序员充分利用它的优势,清楚这些优点是什么。
函数式编程得名于它的基本操作是将函数用于参数。 主程序本身写成一个接收程序输入作为参数并将程序输出作为结果的函数。 通常主函数是根据其他函数来定义,这些函数又是根据更多的函数来定义的,直到最底层的函数是编程语言的原语。 所有这些函数都非常像普通的数学函数,在本文中它们将由普通的等式来定义。 我们在这里使用 Turner 的语言 Miranda ,但是即使你不了解这个语言相关的知识也是是能看明白的。
函数式编程的特点和优点或多或少地会按如下总结。函数式编程不包含赋值语句,所以一旦赋值,变量就不会改变。 更一般来说,函数完全不包含副作用。 函数调用除了计算其结果之外,没有其他作用。 这消除了一个重大错误的来源,也使执行的顺序无关 —— 因为没有副作用更改表达式的值,表达式可以在任何时间进行计算。这减轻了程序员处理控制流程的负担。又因为表达式可以随时进行计算,人们可以通过自由替换变量他们的值,反之亦然 - 也就是说,程序是“引用透明”的。这种自由使函数式程序相比他们的传统同行,在数学上更易处理。
这样的“优势”列表一切都很好,但如果外界对其不太重视,你不会感到惊讶。 它说了很多关于函数式编程不是什么(它没有任务,没有副作用,也没有控制流程),但没有关于它是什么。 函数式程序员听起来更像一位中庸的和尚,否认自己的生活乐趣,希望能使他变得高尚。 对于那些对物质利益更感兴趣的人来说,这些“优势”完全没有说服力。
函数式程序员争辩说,有很大的物质利益 —— 函数式程序员比传统程序员的生产力提高了一个数量级,因为函数式程序要短一个数量级。 但为什么? 基于这些“优点”,我们可以提出的唯一可能的理由是传统的程序由 90% 的赋值语句组成,而在函数式程序中这些可以省略! 这显然是荒谬的。 如果省略赋值语句带来了如此巨大的好处,那么 FORTRAN 程序员二十年前就会这样做了。无论特性有多糟,通过删除它们来使语言更强大在逻辑上是不可能的。
甚至一个函数式程序员也该不满与这些所谓的优势,因为他们无助于利用函数式语言的力量。一个人不可能写出一个绝对没有赋值语句或者绝对引用透明的程序。程序的品质没有衡量标准,因此也没有完美的目标。
显然,函数式编程的这种表征是不够的。 我们必须找到一些东西来代替它 —— 要不仅解释函数式编程的强大功能,而且还清楚地表明了函数式程序员应该努力的方向。
类比结构化编程
对比结构化编程和函数式编程与我们是有帮助的。过去,结构化编程的特点和优势总结如下。结构化编程没有 goto 语句,代码块只有唯一的出入口,相比于非结构化编程更易于追踪。这些结构化编程的“优势”非常类似于之前对函数式编程的形容。他们基本上是否定的陈述,并导致了许多没有结果的论点,比如关于“基本gotos”等。
事后看来,很明显,结构化程序的这些属性虽然有帮助,但并不是问题的核心。 结构化和非结构化程序之间最重要的区别在于结构化程序是以模块化方式设计的。 模块化设计带来了极大的生产力提升。 首先,小模块可以快速简单地进行编码。 其次,通用模块可以重复使用,从而加快后续程序的开发速度。 第三,程序模块可以独立测试,有助于减少调试时间。
gotos的缺席等等,与此无关。 它有助于“小编程”,而模块化设计有助于“大编程”。 因此,人们可以在 Fortran或汇编语言享受结构化编程的好处,即使它需要更多的工作。
目前普遍认为模块化设计是成功编程的关键,最近的语言如 Modula-II 和Ada包含专门设计用于改进模块化的功能。 但是,有一个非常重要的点经常被忽略。 当编写模块化程序来解决问题时,首先将问题分解为子问题,然后解决子问题,最后结合解决方案。 人们如何分解原始问题的方式直接取决于人们如何将解决方案粘合在一起。 因此,为了提高在概念上模块化问题的能力,必须在编程语言中提供新的粘合剂。 复杂的范围规则和单独编译的规定只能帮助文书细节 - 它们永远无法对模块化做出巨大贡献。
我们将在本文的其余部分中讨论功能性语言提供了两种新的非常重要的粘合剂。 我们将举一些可以用新方法模块化的程序例子,从而可以简化程序。 这是功能编程功能的关键 - 它允许改进模块化。 这也是功能程序员必须努力的目标 - 更小,更简单,更通用的模块,与我们将要描述的新粘合剂粘合在一起。
函数的聚合
有两种新的方法将多个函数合成一个更复杂的函数。可以用简单的描述为列表加工过程 —— 增加列表的元素。
listof ∗ ::= Nil | Cons ∗ (listof ∗)
列表要不是空列表 (Nil),要不是一个元素和一个列表的组合(Cons)。Cons 的第一个参数是列表的第一个元素 , 第二个参数是这个列表的剩下的元素组成的列表。列表的元素可以是任何类型。我们会省略 Cons 和 Nil 直接将元素括起来表示一个列表。
[ ] 等价于 Nil
[1] 等价于 Cons 1 Nil
[1, 2, 3] 等价于 Cons 1 (Cons 2 (Cons 3 Nil )
列表的元素可以用可以用一个递归的函数 (sum) 求和 . 这个函数必须能处理两种参数,一种是空列表 Nil, 一种是 Cons。既然零个数字的和是零,我们定义
sum Nil = 0
既然 Cons 可以计算为第一个元素加上剩下的元素,定义
sum (Cons n list) = num + sum list