纯函数与引用透明原则

纯函数

在函数式编程中,纯函数是一个非常重要的概念。简单的来说,纯函数是指对相同的输入值,总是返回相同结果的函数。例如:

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 这个函数满足引用透明原则即为一个纯函数。

为什么需要纯函数?

为什么函数式编程中一定要开发者编写纯函数呢,因为纯函数从某些角度来讲有很多优点,例如:

  1. 便于缓存: 由于纯函数对于相同的输入,总是具有相同的输出。因此纯函数非常容易缓存,只要计算过一次就可以缓存结果,那么下一次有相同的输入便可以直接从缓存中获取,不必重复计算。如果函数是非纯的,那么就算是相同的输入也无法保证输出相同,因此无法缓存。又或者说某个非纯函数中含有 IO 操作,就算是输出结果相同,该函数也需要重复执行来保证最终结果的正确性。
  2. 便于并发编程:由于纯函数不会修改任何外部变量,因此在并发编程中不会存在竞争关系,因此不需要各种锁来保证结果的正确性。
  3. 便于单元测试:纯函数对于相同的输入总是有相同的输出,非常方便的就能构造单元测试样例。并且不会读取任何外部变量,在编写单元测试时不需要构造复杂的测试环境。想象一下如果一个函数里面有一个读取 http 服务的的操作,那么在对这个函数编写单元测试时,还需要先启动一个 http 服务。这简直太复杂了。

总之,纯函数有很多好处,这也是函数式编程中比较推荐存函数的原因。

相关推荐
桦说编程2 小时前
配置快照实现之持久化数据结构
java·后端·函数式编程
石金龙2 天前
用 Ramda 做简易日历
函数式编程·ramda
异常君6 天前
Java 双冒号(::)操作符实战解析与类型推断机制
java·代码规范·函数式编程
安冬的码畜日常1 个月前
【玩转 JS 函数式编程_016】DIY 实战:巧用延续传递风格(CPS)重构倒计时特效逻辑
开发语言·前端·javascript·重构·函数式编程·cps风格·延续传递风格
zidea2 个月前
Rust 闭包:捕获环境的魔法函数
rust·ai编程·函数式编程
独泪了无痕3 个月前
Optional 使用指南:彻底告别 NPE
后端·函数式编程
doodlewind3 个月前
通过 TypeScript 类型体操学习日语语法
typescript·编程语言·函数式编程
谦谦橘子3 个月前
rxjs原理解析
前端·javascript·函数式编程
桦说编程4 个月前
【硬核总结】如何轻松实现只计算一次、惰性求值?良性竞争条件的广泛使用可能超过你的想象!String实际上是可变的?
后端·函数式编程
Oberon4 个月前
从零开始的函数式编程(2) —— Church Boolean 编码
数学·函数式编程·λ演算