概述
在计算机科学中,函数副作用指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响。例如修改全局变量(函数外的变量)或修改参数。
函数副作用会给程序设计带来不必要的麻烦,给程序带来十分难以查找的错误,并且降低程序的可读性。严格的函数式语言要求函数必须无副作用。
下面是函数的副作用相关的几个概念,纯函数(Pure Function)、非纯函数(Impure Function)、引用透明(Referential Transparent)。
纯函数
纯函数(Pure Function)是这样一种函数——输入输出数据流全是显式(Explicit)的。
显式(Explicit)的意思是,函数与外界交换数据只有一个唯一渠道——参数和返回值;函数从函数外部接受的所有输入信息都通过参数传递到该函数内部;函数输出到函数外部的所有信息都通过返回值传递到该函数外部。
f(x)
{
return x + 1
}
f(x)就是纯函数
非纯函数
如果一个函数通过隐式(Implicit)方式,从外界获取数据,或者向外部输出数据,那么,该函数就不是纯函数,叫作非纯函数(Impure Function)。
隐式(Implicit)的意思是,函数通过参数和返回值以外的渠道,和外界进行数据交换。比如,读取全局变量,修改全局变量,都叫作以隐式的方式和外界进行数据交换;比如,利用I/O API(输入输出系统函数库)读取配置文件,或者输出到文件,打印到屏幕,都叫做隐式的方式和外界进行数据交换。
a = 0
q(x) {
b = a
}
q(x)访问了函数的外部变量,q(x)是非纯函数。
p(x){
print“hello”
}
p(x)通过一个I/O API输出了一个字符串,p(x)是非纯函数。
c(x) {
data = readConfig() // 读取配置文件
}
c(x)通过I/O API读取了一个配置文件,c(x)是非纯函数
I/O API可以看作是一种特殊的全局变量。文件、屏幕、数据库等输入输出结构可以看作是独立于运行环境之外的系统外全局变量,而不是应用程序自己定义的全局变量。
参数的函数副作用
上述只讨论了一般的情况,还有一种特殊的情况,我们没有讨论。有些函数的参数是一种In/Out作用的参数,即函数可能改变参数里面的内容,把一些信息通过输入参数,夹带到外界。这种情况,严格来说,也是副作用。也是非纯函数。 比如下面的函数。
process(context) {
a = context.getInfo()
result = calculate(a)
context.setResult(result)
}
这种情况下,context参数同时作为输入和输出渠道。严格意义上,这也叫作副作用。参数只作为输入,返回值只作为输出,这才符合严格的Pure Function定义。
一般情况下,Pure Function的参数应该是只读的。函数不应该改变参数内部的任何状态。
除此之外,还有一种特殊情况。比如,函数调用了获取系统时间的API。这种API是有状态的API,也可以看作是特殊的I/O API。这也是非纯函数(Impure Function)。
纯函数与非纯函数的区别
纯函数的优点
(2)Pure Function相互调用组装起来的函数,还是Pure Function。
(3)应用程序或者运行环境(Runtime)可以对Pure Function的运算结果进行缓存,运算加快速度。
避免函数副作用
首先,全局变量的副作用是很容易避免的。全局变量的读写,可以用参数和返回值代替。我们可以比较容易地消除代码中的全局变量。同样,参数里面放置返回值的情况,也可以很容易用返回值避免。
比较难以处理的情况是I/O的情况。一个应用系统总是需要输入输出的。如果一个应用系统只是在启动的时候,需要输入,在结束的时候,进行输出,那么还好处理一点。我们可以把I/O集中在系统的最外层处理,系统内部不需要处理任何I/O。但这只是一种良好的愿望。常见的应用系统都是交互式的,而且系统经常需要吐出一堆堆的log,经常需要重新接受用户的配置选项更改。
主体程序本身是高贵的,不需要处理Log。编译过程或者运行过程中,AOP系统负责把Advice类里面的Log处理代码夹杂到主体程序中,工作过程非常类似于计算机病毒感染的过程。
于是,进到AOP系统这个大染缸之前,主体程序还是冰清玉洁的;主体程序进入AOP系统,并从AOP系统出来之后,主题程序就已经被感染了,具有了Log等功能。
正如马克.吐温在<竞选州长>中描述的。“你忠实的朋友,过去是正派人,现在却成了伪证犯、小偷、拐尸犯、酒疯子、贿赂犯和讹诈犯的马克•吐温。”
同样的思路,能否应用到Pure Function中呢?
比如,我们可以保持Pure Function的纯洁性,把IO这样的操作,移动到Advice(or Proxy)类里面。然后通过某种类似于AOP的机制,把两者绑定起来。
正如表面看上去,凯撒的归凯撒,上帝的归上帝,世俗王权和宗教神权互不干涉。但实际上,对于权力、金钱的渴望,通常会导致王权神权两种权力的冲突和勾结。
Haskell是Pure Functional Language。Haskell处理IO的方法之一叫作Monad。
Monad是一种臭名昭著的难以理解的东西。
Monad不是我所能理解的东西。我只能对Monad进行猜想。
我猜想,Monad是一种类似于Advice、Proxy的类型定义。
Monad是一种类型定义。可能和C++ Template有些相似。
Monad类型就是专门做IO杂事的特殊类型。除了Monad类型,其他的函数或者类型都是Pure。
如果一个Pure Function,需要输入输出,就必须被Monad包装。