函数式编程

Introduction

编程语言支持用几个不同的方式分解问题:

  • 大部分编程语言是程式的(procedural):程序是告诉计算机如何处理程序输入的指令列表。C,Pascal,甚至 Unix Shells 都是程式语言。
  • 声明性(declarative)语言中,你编写一个要解决的问题的规范,语言实现指出如何高效地完成计算。SQL 可能是你最熟悉的声明性语言。SQL 查询语句描述了你要查询的数据集,SQL 引擎决定是扫描表还是使用索引,哪些子句应该先执行,等等。
  • 面向对象(Object-oriented)的程序操作对象的集合。对象有内部状态并且提供方法以某种方式查询或者修改内部状态。Smalltalk 和 Java 是面向对象的语言。C++ 和 Python也支持面向对象编程,但不强制使用面向对象的特性。
  • 函数式(Functional)编程将问题分解为一组函数。原则上,函数式编程只接受输入并产生输出,对于一个给定的输入没有任何内部状态会影响输出。著名的函数式语言有 Ml 系列(标准 ML,OCaml 和其他变体)和 Haskell。

一些计算机语言的设计者选择强调一种特定的编程方式。这通常使得使用不同的方式编写程序变得困难。其他语言是支持多种不同方式的多范式语言。Lisp,C++ 和 Python 是多范式的;使用这些语言,你可以编写主要是过程的,面向对象的,或者函数式的程序或者库。在大型程序中,可能使用不同的方式编写不同的部分;例如,GUI 可以是面向对象的,而处理逻辑是程式的或者函数式的。

在函数式编程中,输入流经一系列函数。每一个函数对其输入进行操作,产生一些输出。函数式风格不鼓励有副作用的函数(修改内部状态或进行在函数返回值中不可见的其他操作)。完全没有副作用的函数称为纯函数(purely functional)。避免副作用意味着不要使用会在程序运行过程中更新的数据结构;函数的的输出必须只取决于它的输入。

一些语言对纯度非常严格,甚至没有诸如 a=3c = a + b 的赋值语句。但要避免所有的副作用是很难的。例如,打印到屏幕或者写入磁盘文件都是副作用。比如在 Python 中调用 print()time.sleep() 函数都返回无效值;调用它们只是为了它们发送一些文本到屏幕或者暂停执行一会的副作用。

使用函数式风格编写的 Python 程序通常不回陷入避免所有 I/O 操作或赋值的极端。相反,它们提供对外的函数借口,但内部使用非函数式的特性。例如,函数的执行仍会对本地变量赋值,但不会修改全局变量或者有其他的副作用。

函数式编程可以认为是面向对象编程的对立面。对象是包含一些内部状态和一组允许你修改其状态的方法的胶囊。(面向对象)程序由进行状态的正确更改组成。函数式编程想要尽可能的避免状态改变,并且使用函数间的数据流工作。在 Pyhton 中你可以通过编写组合这两种方法,这些函数接收并返回表示应用程序中对象的实例(e-mail 信息,事务,等等…)。

函数式的设计可能看起来像是一个奇怪的约束。为什么要避免对象和副作用呢?函数式风格有以下理论和实践中的优势:

  • Formal provability(形式可证明)
  • Modularity(模块化的)
  • Composability(可组合的)
  • Ease of debugging and testing(易于调试和测试)

Formal provability

理论上的好处是构建证明函数式程序正确的数学证明更容易。

很长的一段时间里,研究人员一直对寻找用数学证明程序正确性的方法感兴趣。这与通过大量输入和正确的输出测试程序,或者通过阅读程序源代码确定代码正确不同;它的目标是严格证明程序对于所有可能的输入都产生正确的结果。

用于证明程序正确性的技术是,记下常量,输入数据和永远为真的程序变量的属性。对于每一行代码,如果常量 X 和 Y 在该行代码执行前为真,那么在该行代码执行后的稍有不同的常量 X‘ 和 Y‘ 为真。这持续到程序的结束,此时常量应该符合程序输出的所需条件。

函数式编程避免赋值的发生是因为它难以处理赋值;赋值可以在赋值打破为真的常量,而不产生任何新的可以继续传播的常量。

不幸的是,证明程序正确在很大程度上是不切实际的,无关 Pythno 软件。即便是很小的程序也需要几页纸长的证明;一个中等复杂度的程序的正确性证明工作量是巨大的,你日常使用的程序(Python 解析器,XML 页面,web浏览器)很少或者说没有一个能够被证明为正确的。即使你写下或者生成一个证明,也会有验证证明的问题;可能其中有一个错误,而你错误地认为你已经证明了程序的正确性。

Modularity

函数式编程的一个更实际的好处是它迫使你把问题分解为小块。因此程序更加模块化。比起一个执行复杂转换的大函数,指定、编写一个只做一件事的小函数更加容易。小函数也更容易阅读和检查错误。

Ease of debugging and testing

测试和调试函数式风格的程序更容易。

调试简单是因为函数通常很小而且分工明确。当程序中断时,每一个函数都是一个入口,你可以检查其中的数据是否正确。你可以查看中间输入和输出来快速判断出导致程序错误的函数。

测试更加容易因为每一个函数都是潜在的单元测试项目。函数不依赖于在进行测试前需要复制的系统状态;相反,你只需要合成正确的输入然后检查输出符合预期。

Composability

处理函数式风格的程序,你会编写许多具有不同输入和输出的函数。其中的一些函数不可避免的专门用于特定的应用,但其他的在很多各种程序中都有用。例如,一个接受一个文件路径并返回文件中所有 XML 文件的函数,或者一个接受一个文件名并返回它的内容的函数,能被应用到许多不同的场景。

随着时间的推移,你会构建一个个人的工具库。通常,你将通过在新配置中安排现有函数并编写一些专门用于当前任务的函数来组装新程序。