什么是函数式编程?
函数式编程是一种编程范式,代表着我们通过什么样的方法去理解和设计我们的程序。比如面向对象编程也是一种编程范式,在这种思想下我们在设计我们的程序时我们会把所有概念抽象成一个对象,他具有哪些属性和方法,在什么地方我们需要查看或者修改这些属性,或者需要调用他的方法。而函数式编程顾名思义,就是用函数的思想来设计我们的程序,这里的函数是真正数学意义中的函数,及y=f(x),不同的自变量x都有对应的因变量y。我们的程序看起来就像在计算一个我们想要的结果。
为什么是函数式编程?
当我们的程序是一个个函数时,即意味着这个程序单纯地代表着输入到输出的映射,在程序设计中我们称之为纯函数:
纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。
什么是副作用呢?副作用是指在计算结果时,发生的对程序状态的改变,包括但不限于:
- 更改文件系统
- 往数据库插入记录
- 发送一个 http 请求
- 可变数据
- 打印/log
- 获取用户输入
- DOM 查询
- 访问系统状态
为什么要追求纯函数呢?首先纯函数对于相同输入,永远得到相同输出,也就意味着他不依赖于任何外部的状态。其次纯函数除了计算我们想要的结果之外不会去修改任何的外部状态,这意味着他不会去影响其他的代码。这两个特点让纯函数的代码非常易读和易测试,同时减少了很多出现bug的机会。然而除此之外,纯函数还会因其特点产生其他的一些好处:
可缓存性
因为纯函数每个输入都对应一个输出,那么我们就可以把输入当作一种key来存储结果,形成一个缓存的效果。例如:
// 这里我们先构造一个方法memorize,它接收一个方法然后返回这个方法的可缓存版本
const memorize = f => {
const cache = {};
return function () {
const arg_str = JSON.stringify(arguments);
cache[arg_str] = cache[arg_str] || f(...arguments);
return cache[arg_str]
}
}
// 现在我们构造一个纯函数方法,并且通过memorize让它变得可缓存
const square = x => x * x;
const cacheableSquare = memorize(square);
const a = cacheableSquare(5);
// 这次当我们调用cacheableSquare时,不再真的进行计算而是从cache中直接返回了计算结果
const b = cacheableSquare(5);
可并行性
这个很好理解,因为纯函数不使用和操作任何状态,即不会出现竞争态,所以纯函数的程序可以放心的并行。
函数组合Compose以及柯里化
为了避免不断地写诸如const x = square(toNumber('0'))类似这样的难以阅读的代码,我们可以使用compose来将我们需要使用的函数进行组合,形成一个大的函数来处理数据,感觉就像用一堆小组件来构造一个大的组件一样:
const compose = (f, g) => x => f(g(x))
这样我们之前的方法就可以改写成
const squareString = compose(square, toNumber);
const result = squareString('0');
可以看出来,如果我们用来组合的函数,最好是只接收一个参数,这个时候我们就需要对多参函数进行柯里化,来满足让它可以正常compose的需求了。
柯里化即把f(x, y)转换成f(x)(y)的过程,这里我们举一个简单的例子:
// 这里我们构造一个普通的add方法
const add = (x, y) => x + y;
// 经过柯里化后,这个方法应该长这个样子
const curryAdd = x => y => x + y;
// 现在我们可以正常使用curryAdd这个方法
const res = curryAdd(5, 6)
// 也可以通过创建另一个方法来进行add操作
const add5 = curryAdd(5);
const res = add5(6)
Container/Functor
functor(函子)是一个数学概念,这里我不想过多介绍这个数学的概念,让我们把目光聚集在一种函数式的写法,创造一个Container上:
// Container相当于一个class,它通过__value来保存一个值
const Container = (x) => {
this.__value = x
}
// 我们赋予Container一个方法map,map方法可以返回一个新的改变了__value的Container
Container.prototype.map = f => new Container(f(this.__value))
// new Container这种写法看起来不是很函数式,我们可以做一些改动
Container.of = x => new Container(x)
// 那么map方法就可以变成
Container.prototype.map = f => Container.of(f(this.__value))
const num = Container.of(5);
// functor.__value == 5
const square = x => x * x;
const squareNum = num.map(square)
// squareNum.__value = 25
这个Container就可以称作一个functor,我粗浅地理解为Container中的方法最终依然返回一个新的Container类型的数据,就如同js中的map,slice一样。
const square = x => x * x;
const add5 = x => x + 5;
const result = Container.of(6).map(square).map(add5)
// result.__value == 41