函数式编程
它其实就是强调在编程过程中把更多的关注点放在如何去构建关系。通过构建一条高效的建流水线,一次解决所有问题。而不是把精力分散在不同的加工厂中来回奔波传递数据。
函数式编程特点
函数是“一等公民”
这是函数式编程得以实现的前提,因为我们基本的操作都是在操作函数。这个特性意味着函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。
声明式编程
函数式编程大多时候都是在声明我需要做什么,而非怎么去做。这种编程风格称为 声明式编程 。这样有个好处是代码的可读性特别高,因为声明式代码大多都是接近自然语言的,同时,它解放了大量的人力,因为它不关心具体的实现,因此它可以把优化能力交给具体的实现,这也方便我们进行分工协作。
懒惰执行
函数只在需要的时候执行,即不产生无意义的中间变量。
无状态和数据不可变
这是函数式编程的核心概念:
- 数据不可变: 它要求你所有的数据都是不可变的,这意味着如果你想修改一个对象,那你应该创建一个新的对象用来修改,而不是修改已有的对象。
- 无状态: 主要是强调对于一个函数,不管你何时运行,它都应该像第一次运行一样,给定相同的输入,给出相同的输出,完全不依赖外部状态的变化。
为了实现这个目标,函数式编程提出函数应该具备的特性:没有副作用和纯函数。
没有副作用
在我们函数中最主要的功能当然是根据输入返回结果,而在函数中我们最常见的副作用就是随意操纵外部变量。
保证函数没有副作用,一来能保证数据的不可变性,二来能避免很多因为共享状态带来的问题。
纯函数 (pure functions)
其实纯函数的概念很简单就是两点:
- 不依赖外部状态(无状态): 函数的的运行结果不依赖全局变量,this 指针,IO 操作等。
- 没有副作用(数据不变): 不修改全局变量,不修改入参。
所以纯函数才是真正意义上的 “函数”, 它意味着相同的输入,永远会得到相同的输出。
纯函数的意义是什么?
-
便于测试和优化:这个意义在实际项目开发中意义非常大,由于纯函数对于相同的输入永远会返回相同的结果,因此我们可以轻松断言函数的执行结果,同时也可以保证函数的优化不会影响其他代码的执行。这十分符合测试驱动开发 TDD(Test-Driven Development ) 的思想,这样产生的代码往往健壮性更强。
-
可缓存性:因为相同的输入总是可以返回相同的输出,因此,我们可以提前缓存函数的执行结果,有很多库有所谓的
memoize函数。 -
自文档化:由于纯函数没有副作用,所以其依赖很明确,因此更易于观察和理解(配合后面介绍的 [类型签名](#hindly-milner 类型签名)更佳)。
-
更少的 Bug:使用纯函数意味着你的函数中不存在指向不明的 this,不存在对全局变量的引用,不存在对参数的修改,这些共享状态往往是绝大多数 bug 的源头。
流水线的构建
如果说函数式编程中有两种操作是必不可少的那无疑就是柯里化(Currying)和函数组合(Compose),柯里化其实就是流水线上的加工站,函数组合就是我们的流水线,它由多个加工站组成。
柯里化的意思是将一个多元函数,转换成一个依次调用的单元函数。
f(a,b,c) → f(a)(b)(c)
为什么这个单元函数很重要?还记得我们之前说过的,函数的返回值,有且只有一个嘛? 如果我们想顺利的组装流水线,那我就必须保证我每个加工站的输出刚好能流向下个工作站的输入。因此,在流水线上的加工站必须都是单元函数。
部分函数应用 vs 柯里化
部分函数应用强调的是固定一定的参数,返回一个更小元的函数。通过以下表达式展示出来就明显了:
// 柯里化
f(a,b,c) → f(a)(b)(c)
// 部分函数调用
f(a,b,c) → f(a)(b,c) / f(a,b)(c)
柯里化强调的是生成单元函数,部分函数应用的强调的固定任意元参数,而我们平时生活中常用的其实是部分函数应用,这样的好处是可以固定参数,降低函数通用性,提高函数的适合用性。
高级柯里化
可以根据你输入的参数个数,返回一个柯里化函数/结果值。即,如果你给的参数个数满足了函数条件,则返回值。这样可以解决一个问题,就是如果一个函数是多输入,就可以避免使用 (a)(b)(c) 这种形式传参了。
我们可以用高级柯里化去实现部分函数应用,但是柯里化不等于部分函数应用。
函数组合概念
函数组合的目的是将多个函数组合成一个函数。它让代码变得简单而富有可读性,同时通过不同的组合方式,我们可以轻易组合出其他常用函数,让我们的代码更具表现力。
实践经验
柯里化中把要操作的数据放到最后
因为我们的输出通常是需要操作的数据,这样当我们固定了之前的参数(我们可以称为配置)后,可以变成一个单元函数,直接被函数组合使用,这也是其他的函数式语言遵循的规范:
const split = curry((x, str) => str.split(x));
const join = curry((x, arr) => arr.join(x));
const replaceSpaceWithComma = compose(join(','), split(' '));
const replaceCommaWithDash = compose(join('-'), split(','));
函数组合中函数要求单输入
函数组合有个使用要点,就是中间的函数一定是单输入的,这个很好理解,之前也说过了,因为函数的输出都是单个的(数组也只是一个元素)。