深入浅出函数副作用

 概述

    在计算机科学中,函数副作用指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响。例如修改全局变量(函数外的变量)或修改参数。

    函数副作用会给程序设计带来不必要的麻烦,给程序带来十分难以查找的错误,并且降低程序的可读性。严格的函数式语言要求函数必须无副作用。

    下面是函数的副作用相关的几个概念,纯函数(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可以看作是一种特殊的全局变量。文件、屏幕、数据库等输入输出结构可以看作是独立于运行环境之外的系统外全局变量,而不是应用程序自己定义的全局变量。

    纯函数内部有隐式(Implicit)的数据流,这种情况叫做副作用(Side Effect)。我们可以把副作用想象为潜规则。上述的I/O,外部变量等,都可以归为副作用。因此,纯函数的定义也可以写为,没有副作用的函数,叫做纯函数。

参数的函数副作用

    上述只讨论了一般的情况,还有一种特殊的情况,我们没有讨论。有些函数的参数是一种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)。 

纯函数与非纯函数的区别

    1.纯函数(Pure Function)就是那种一根筋的理想主义者,绝不搞歪门邪道,没有什么灰色收入,数据入口和出口只有一条唯一途径——参数和返回值。只要卡住参数、返回值的出入口,就可以监控所有的数据流向。这对征税很有好处。比如一般的工薪阶层,只有工资一条收入渠道,扣税是银行直接代缴的。
    Impure Function就不一样了,为了行事方便,大开后门,各种暗地手段无所不用其极,路子很宽很野。从函数签名(函数名,参数列表,返回值)定义上,你不知道Impure Function内部实现中有多少潜在的条数据交换的通路。监控Impure Function的数据流向是比较困难的。对Impure Function征税,也是比较困难的。只能期望灰色收入的人群自己申报。
    2. 纯函数和不纯的函数可以大致类比为组合电路和时序电路。纯函数和组合电路是没有状态的,不纯的函数和时序电路是有状态的。
      组合电路的计算依赖是门电路的依赖,纯函数的计算依赖是表达式依赖。组合电路的计算速度是非常快的,只与参与计算的门电路的延迟有关,都是并行的,和时钟没有关系。纯函数天生适合并行计算,其计算速度和硬件电路的并行能力有关。
      时序电路的计算依赖是前一拍时钟的计算结果,不纯函数的计算依赖是前一个不纯函数的计算结果,都是串行的。时序电路的计算速度和时钟有关系,时钟越快则计算速度越快。不纯的函数的计算是串行的,因此其计算速度和硬件电路的并行能力无关。
      我们的现实世界也是有时序的,这些时序由永不停止的时间驱动着。 

纯函数的优点

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

避免函数副作用 

    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包装。 
    
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值