函数式编程(Functional Programming)

@wanqiuz 2018-04-26 10:32:42发表于 wanqiuz/blog-articles 函数式编程范式

1 函数式编程的由来

遥想当年,盘古开天辟地,创造了计算机世界,有两位巨擘对计算机的运算能力做了模型化描述。

一位是阿兰.图灵(Alan Turing),就是奥斯卡奖电影《模仿游戏》里的图灵,计算机软件业界的祖师爷,他是一个gay,没错,我们的祖师爷是gay,所以干这行的真不应该歧视同性恋:-) 图灵提出“图灵机”的概念,大概意思就是说,假设有一个纸带和一个打孔机,然后有一套指令,能够控制打孔机在纸带上移动、能够读取当前位置是否打了孔、能够在当前位置打一个孔,这就是一个图灵机,假设一个问题能够靠这个纸带+打孔机+指令的方式解决,那就说明这个问题是“可计算的”。当然,这只是一个理论模型,实际上没人会用这种机械方式来制造计算机。

另外一个位巨擘,是邱奇(Alonzo Church)(更正:Church翻译为邱奇是业界通用译法,和当过英国首相的那个丘吉尔不同,丘吉尔是Churchill),这个邱奇是个数学家,他提出了Lambda演算(Lambda Calculus)的概念,用函数组合的方式来描述计算过程,换句话来说,如果一个问题能够用一套函数组合的算法来表达,那就说明这个问题是可计算的。

无论是“图灵机”,还是“Lambda演算”,都能够用来表达计算过程,但是这两种模型都没有被采用来生产计算机。

为什么会这样呢?

根据马克思经济学,生产力是决定性力量,计算机世界设计成这样,是因为经济问题。

按照图灵机那样建造机械方式的计算机肯定不行,而按照Lambda演算这样的方式来构建计算机也不行,因为那样造价太高。

现有的计算机体系结构都是CPU中内嵌一些寄存器(register),CPU指令就是通过这些寄存器来运算结果,寄存器数量有限,因为CPU上的寄存器很贵,从经济角度,导致一个程序要时不时要把运算结果从寄存器移到内存里面去,这一切都要由程序指令来控制,这就是命令式的编程方式(Imperative Programming),一步一步怎么做都要描述清楚。

正因为计算机诞生之初寄存器生产代价很大,所以这种很“节约”的指令方式被生产硬件的厂商采用,这种机器指令的形态决定了汇编语言的形态,汇编语言的形态决定了C语言的形态,天下武功出少林,天下语言出C语言,C语言的风格又影响了很多语言,以至于命令式编程达到了统治地位。

在计算机创世之初,并没有计算机科学家,因为没有鸡也就没有鸡蛋,是数学家、物理学家和电子工程师开辟了计算机这个新的世界,数学家的思维自上而下,首先构想的是数学模型,然后才是如何落地实现,物理学家和电子工程师的思维防止自下而上,先想的是如何实现硬件,然后才去迎合数学的描述。

很显然,自下而上的方式成为工业的选择。

v2-9cdb127ccedc649f7e6ea566f1494426_r
不过,虽然没有直接的硬件支持,函数式编程并没有像恐龙一样绝种,比如LISP,这种语言一直存活,连Emacs编辑器(也许有人会说Emacs不只是编辑器)都是LISP语言写的。不过,函数式编程语言一直也没有特别被广为接受,因为通过一套完全不同思维方式的机器指令来模拟函数式的理想,肯定会消耗一些性能,不过,现在计算机硬件发展到足够强大的底部,我们终于可以忽略不计这些性能差异了。

总之,是时候学习函数式编程了。

2 函数式编程的特点

2.1 函数是一等公民

函数和其它数据类型处于平等地位,不搞特殊,可以把函数赋值给其他变量,也可以作为参数传入另一个函数,或者作为别的函数的返回值。

2.2 数据是不可变的(Immutable)

在纯种的函数式编程语言中,数据是不可变的,或者说没有变量这个概念,所有的数据一旦产生,就不能改变其中的值,如果要改变,那就只能生成一个新的数据。

在React中,强调一个组件不能去修改传入的prop值,也是遵循Immutable的原则。
在Redux中,更是强调Immutable的作用,每个reducer不能够修改state,只能返回一个新的state。

2.3 使用纯函数(pure functions)

所谓纯函数,就是和数学概念上的函数一样的函数,没有任何副作用(side effects),输出完全由输入决定。

在React中,组件的render函数应该是一个纯函数(你要非说只需要是一个幂等函数,也行),只有这样,组件渲染的结果才只和state/props有关系,遵循UI=f(state)这个公式。

2.4 函数递归调用

不存在for/while这样的循环语句,需要循环执行就要靠递归。尾递归。

尾调用:指某个函数的最后一步是调用另一个函数。
尾递归:函数尾部调用自身。

而且只用"表达式",不用"语句"。每一步都是单纯的运算,而且都有返回值。

"表达式"(expression)是一个单纯的运算过程,总是有返回值;
"语句"(statement)是执行某种操作,没有返回值。

2.5 函数只接受一个参数

比如,在Redux中,要写一个Middleware,代码是这个样子。

const someMiddleware = store => next => action => {
  // 实现middleware
};

3 函数式编程的意义

函数式编程到底有什么好处,为什么会变得越来越流行?

3.1 代码简洁,开发快速

3.2 接近自然语言,易于理解

函数式编程的自由度很高,可以写出很接近自然语言的代码。

流计算模式: 这个概念来自于SICP里面的第3章,可以理解为unix里面的pipline,使用它可以让代码具有申明式的语义化、模块化,更加富有表现力。

以javascript为例,设计好的风格的代码表现如下:

getAsyncStockData()
  .filter(quote => quote.price > 30)
  .map(quote => quote.price)
  .forEach(price => console.log(`Prices higher than $30: ${price}`));

3.3 更方便的代码管理

函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。因此,每一个函数都可以被看做独立单元,很有利于进行单元测试(unit testing)和除错(debugging),以及模块化组合。

3.4 易于"并发编程"

函数式编程不需要考虑"死锁"(deadlock),因为它不修改变量,所以根本不存在"锁"线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,部署"并发编程"(concurrency)。
请看下面的代码:

  var s1 = Op1();
  var s2 = Op2();
  var s3 = concat(s1, s2);

由于s1和s2互不干扰,不会修改变量,谁先执行是无所谓的,所以可以放心地增加线程,把它们分配在两个线程上完成。其他类型的语言就做不到这一点,因为s1可能会修改系统状态,而s2可能会用到这些状态,所以必须保证s2在s1之后运行,自然也就不能部署到其他线程上了。
多核CPU是将来的潮流,所以函数式编程的这个特性非常重要。

3.5 代码的热升级

函数式编程没有副作用,只要保证接口不变,内部实现是外部无关的。所以,可以在运行状态下直接升级代码,不需要重启,也不需要停机。Erlang语言早就证明了这一点,它是瑞典爱立信公司为了管理电话系统而开发的,电话系统的升级当然是不能停机的。

参考并感谢: