函数的副作用及其他

自:http://buaawhl.iteye.com/blog/177402

函数的副作用及其他 
Pure Function、Impure Function、副作用、Referential Transparent 

纯函数(Pure Function)是这样一种函数——输入输出数据流全是显式(Explicit)的。 
显式(Explicit)的意思是,函数与外界交换数据只有一个唯一渠道——参数和返回值;函数从函数外部接受的所有输入信息都通过参数传递到该函数内部;函数输出到函数外部的所有信息都通过返回值传递到该函数外部。 
如果一个函数通过隐式(Implicit)方式,从外界获取数据,或者向外部输出数据,那么,该函数就不是Pure Function,叫作Impure Function。 
隐式(Implicit)的意思是,函数通过参数和返回值以外的渠道,和外界进行数据交换。比如,读取全局变量,修改全局变量,都叫作以隐式的方式和外界进行数据交换;比如,利用I/O API(输入输出系统函数库)读取配置文件,或者输出到文件,打印到屏幕,都叫做隐式的方式和外界进行数据交换。 
如果用社会现象来比喻,显式(Explicit)就是显规则,隐式(Implicit)就是潜规则。 
Pure Function就是那种一根筋的理想主义者,绝不搞歪门邪道,没有什么灰色收入,数据入口和出口只有一条唯一途径——参数和返回值。只要卡住参数、返回值的出入口,就可以监控所有的数据流向。这对征税很有好处。比如一般的工薪阶层,只有工资一条收入渠道,扣税是银行直接代缴的。 
Impure Function就不一样了,为了行事方便,大开后门,各种暗地手段无所不用其极,路子很宽很野。从函数签名(函数名,参数列表,返回值)定义上,你不知道Impure Function内部实现中有多少潜在的条数据交换的通路。监控Impure Function的数据流向是比较困难的。对Impure Function征税,也是比较困难的。只能期望灰色收入的人群自己申报。 
下面举几个例子。 

f(x) { return x + 1 } 
f(x)函数就是Pure Function。 

a = 0 
q(x) { 
  b = a 

q(x) 访问了函数外部的变量。q(x)是Impure Function。 

p(x){ 
print “hello” 

p(x)通过I/O API输出了一个字符串。p(x)是Impure Function。 

c(x) { 
  data = readConfig() // 读取配置文件 

c(x)通过I/O API读取了配置文件。c(x)是Impure Function。 

Pure Function内部有隐式(Implicit)的数据流,这种情况叫做副作用(Side Effect)。我们可以把副作用想象为潜规则。上述的I/O,外部变量等,都可以归为副作用。因此,Pure Function的定义也可以写为,没有副作用的函数,叫做Pure Function。 
I/O API可以看作是一种特殊的全局变量。文件、屏幕、数据库等输入输出结构可以看作是独立于运行环境之外的系统外全局变量,而不是应用程序自己定义的全局变量。 
上述只讨论了一般的情况,还有一种特殊的情况,我们没有讨论。有些函数的参数是一种In/Out作用的参数,即函数可能改变参数里面的内容,把一些信息通过输入参数,夹带到外界。这种情况,严格来说,也是副作用。也是Impure Function。 
比如下面的函数。 
process(context) { 
a = context.getInfo() 
result = calculate( a ) 
context.setResult( result ) 

这种情况下,context参数同时作为输入和输出渠道。严格意义上,这也叫作副作用。参数只作为输入,返回值只作为输出,这才符合严格的Pure Function定义。 
一般情况下,Pure Function的参数应该是只读的。函数不应该改变参数内部的任何状态。 
除此之外,还有一种特殊情况。比如,函数调用了获取系统时间的API。这种API是有状态的API,也可以看作是特殊的I/O API。这也是Impure Function。 

Pure Function有这么多限制,那么Pure Function到底有什么好处呢?难道就是监控数据流方便?还是征税方便? 
我能想到的,Pure Function的好处主要有几点: 
(1)无状态,Stateless。线程安全。不需要线程同步。 
(2)Pure Function相互调用组装起来的函数,还是Pure Function。 
(3)应用程序或者运行环境(Runtime)可以对Pure Function的运算结果进行缓存,运算加快速度。 

上述的好处(3)需要说明一下。Pure Function的输入、输出都是固定的。输入是同样的参数,输出结果一定是同样的结果。而Impure Function的副作用是无法用(参数、结果)缓存的。参见前面的例子。 
没有副作用,也可以叫做Referential Transparent(引用透明)。 
Referential Transparent的意思好像这样的。在一个范围内,一个变量或者表达式出现在多个地方,那么这些地方的值都是一样的,可以进行值替换。 
比如, 
f(x) { 
  a = f(x) 
  b = f(x) 


编译器或者运行环境发现程序里面出现了两次f(x),就可以放心地用第一个f(x)的结果,代替另一个f(x)。(这是我的个人理解,不能确保就是如此。) 

--------------------------------------- 
副作用、状态、I/O、AOP、Monad 

Pure Function是无状态的。很好,很干净。清净琉璃体,玲珑剔透心。 
Haskell就是这样一种理想主义的Pure Functional Language(纯函数式编程语言)。 
但是,世界是复杂的,有很多潜规则,华丽的外表下有很多暗流在涌动。无状态的Pure Function不足以表达充满了状态和副作用的现实需求。 
我们来看一看,哪些副作用是可以避免的,那些副作用是无法避免的。 
首先,全局变量的副作用是很容易避免的。全局变量的读写,可以用参数和返回值代替。我们可以比较容易地消除代码中的全局变量。同样,参数里面放置返回值的情况,也可以很容易用返回值避免。 
比较难以处理的情况是I/O的情况。一个应用系统总是需要输入输出的。如果一个应用系统只是在启动的时候,需要输入,在结束的时候,进行输出,那么还好处理一点。我们可以把I/O集中在系统的最外层处理,系统内部不需要处理任何I/O。但这只是一种良好的愿望。常见的应用系统都是交互式的,而且系统经常需要吐出一堆堆的log,经常需要重新接受用户的配置选项更改。 
I/O又是一种非常特殊的全局变量。I/O设备(或者结构)独立于应用系统之外(比如,文件,网络,数据库系统)。应用系统很难在程序设计的层次上,用参数、返回值代替I/O API。 
输出部分还是比较容易收集。我们可以把所有的输出都收集到一个巨大的List结构中,作为返回值,一层层返回到最外层。最外层统一把List中的数据全部输出到对应设备。 
输入部分,就难办了。应用程序可能随时需要访问一下配置文件、数据库、系统时钟等设备。我们无法预料到程序内部什么时候需要读取什么样的设备和信息,我们无法提前准备这些输入信息作为参数。 
怎么办呢?我们联想一下。AOP(Aspect Oriented Programming)最著名的例子就是Log(系统日志)。 
在AOP中,函数出入口的Log等脏活累活都可以统一集中地交给几个Advice类处理。Advice类就是那种Interceptor、Filter、Proxy之类的东西。通常会有intercept、around等方法。 
主体程序本身是高贵的,不需要处理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包装。 
我们可以想象几个IO Monad Proxy的例子。 
InputProxy ( function ) {  // function 就是被截获的Pure Function 
a = readSomething // 读取输入设备 
f( a ) 


OutputProxy( function) { // function 就是被截获的Pure Function 
b = function() 
print b 


在Monad Proxy中,(我猜想)应该只能存在一条输入输出语句。如上例所示。 
如果要同时输入输出。那么就必须把上述的Monad Proxy串起来。 
比如,先输入,再输出。应该这么写。OutputProxy( InputProxy ( function ) ) 
如果用嵌套函数的写法,应该写成这样。 
OutputProxy() { 
b = InputProxy() { 
a = readSomething() 
function( a ) { 
// process a 



print b 


为什么一定要保证一个函数中的输入输出语句只有一条?为什么一定要写成这样? 
我猜想,这可能和Haskell的Referential Transparent的特性有关。Haskell支持Referential Transparent,支持同名变量或者同字符串表达式的任意替换。在一定程度上,Haskell程序的代码是顺序无关的。如果是Pure Function,编译器处理起来比较容易。 
如果引入了和外界交换状态的输入输出语句,就必须强制代码的顺序性了。必须保证代码顺序执行。在Haskell中,要强制顺序,只能通过函数的一层层调用来保证。 
正如前面所说的。Haskell中, 
a = f(x) 
b = g(x) 
这种同一层的两次f(x)调用,不一定能够保证这两条语句的执行顺序。 
要保证f在g之前执行,我们只能写成 g ( f (.. ) ) 的形式,才能保证 f 在 g 之前执行。 

Haskell的do语句可能就是把看起来是平级顺序语句转化成层层嵌套调用的语法糖。 
比如, 
do 
readSomething 
print something 

实际上会被翻译成函数调用的嵌套形式。 
g() { 
readSomething 
f() { 
print something 



Monad类型定义其实就是为了生成这样的函数调用嵌套结构。这个生成过程好像也叫作bind(绑定)。 
bind就有点类似于AOP系统的那种夹杂绑定过程,把干净的东西和副作用揉合到一起。 
注,上述只是猜想。 
那些城里人动不动就搞些Category之类的名词吓唬我们乡下人。不讲人话,总是讲神话。 
我希望,咱老百姓能够讲述自己的故事。不整那些神鬼莫测的名词,也照样能把话说清楚。 
显然,这是一种奢望。至少我还说不清楚。话语权还是握在城里人的手里。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值