纯函数
在函数式编程中,纯函数是一个非常重要的概念。简单的来说,纯函数是指对相同的输入值,总是返回相同结果的函数。例如:
javascript
function add(x, y){
return x + y
}
let base = 0
function incr(num){
base += 1
retrun num + base
}
其中 add
是一个纯函数,因为无论在什么情况下调用该函数,该函数都返回输入参数的和。而 incr
则不是一个纯函数,因为随着调用次数增加,相同的输入会得到不同的结果。
使得函数"不纯"的操作,称之为副作用。上面例子中修改函数外部变量就是一种副作用。常见的副作用还有 IO 操作,例如读写文件,读取控制台,输出信息到控制台等。
引用透明原则
将输出信息到控制台是非常常见的操作,但它也确实是一种副作用。这从直觉上有一点难以理解,毕竟打印到控制台并不会修改任何变量,为什么会将它称为副作用呢?在解释之前,需要先介绍一个概念即引用透明原则 。引用透明原则可以用来判断一个函数是否是纯函数的。一个引用透明的函数,总可以将表达替换为对应的结果,而不使整个程序发生任何改变 。例如对于表达式 add(3, 4) + 5
可以将 add(3, 4)
替换为 7,而整个程序未发生任何改变。但如果在 add 函数中添加一个打印语句:
javascript
function addAndPrint(x, y){
console.log(x + y)
return x + y
}
则表达式 addAndPrint(3, 4) + 5
无法直接将 addAndPrint(3, 4)
替换为 7,因为替换之后控制台的输出消失了,因此程序的结果发生了改变。
很多人会疑惑,只是少了一条控制台的输出,无伤大雅。为什么需要将这个行为看作是副作用呢?换个思路想一想,如果将输出到控制台改为写文件,或者向某个接口发出一次请求,那么这种情况下,意味着可能程序需要输出的文件缺少一部分结果或者需要发送的请求并没有发送。
来看一个稍微复杂一点的例子:
javascript
base = 1
function incr(num){
return () => {
base += 1
return num + base
}
}
这可能有点奇怪,但是 incr
同样是一个纯函数。incr 似乎引用了外部变量 base,并且对其进行了修改。但是仔细看,incr 并没有直接修改 base 的值,而是返回了一个修改 base 值的函数 。这看起来有点作弊,但是此时的 incr 确确实实是一个纯函数。因为将 incr(num)
替换为 () => {base += 1; return num + base}
后程序的行为不会有任何改变。那么 incr 这个函数满足引用透明原则即为一个纯函数。
为什么需要纯函数?
为什么函数式编程中一定要开发者编写纯函数呢,因为纯函数从某些角度来讲有很多优点,例如:
- 便于缓存: 由于纯函数对于相同的输入,总是具有相同的输出。因此纯函数非常容易缓存,只要计算过一次就可以缓存结果,那么下一次有相同的输入便可以直接从缓存中获取,不必重复计算。如果函数是非纯的,那么就算是相同的输入也无法保证输出相同,因此无法缓存。又或者说某个非纯函数中含有 IO 操作,就算是输出结果相同,该函数也需要重复执行来保证最终结果的正确性。
- 便于并发编程:由于纯函数不会修改任何外部变量,因此在并发编程中不会存在竞争关系,因此不需要各种锁来保证结果的正确性。
- 便于单元测试:纯函数对于相同的输入总是有相同的输出,非常方便的就能构造单元测试样例。并且不会读取任何外部变量,在编写单元测试时不需要构造复杂的测试环境。想象一下如果一个函数里面有一个读取 http 服务的的操作,那么在对这个函数编写单元测试时,还需要先启动一个 http 服务。这简直太复杂了。
总之,纯函数有很多好处,这也是函数式编程中比较推荐存函数的原因。