前言
为了清楚起见,请记住,副作用不是必需的坏事,有时副作用是有用的(尤其是在函数式编程范式之外)。
今天聊一聊函数式编程中的隔离思想,它所想隔离的就是“副作用”
我们先从其他角度来聊一聊副作用这个概念。
生活中的副作用
如果我听到副作用这个词后,第一反应是吃药 。
老话说是药三分毒,其中三分毒则为副作用。就比如你感冒了,吃了一些西方某些国家研制的专利药品,然后感冒好了,但是感冒好了之后发现自己秃顶 了。
那么可以说秃顶就是这个感冒药的副作用。
我们来捋一下这个逻辑
感冒好没好?
答:好了这药算不算感冒药 ?
答:算感冒药不吃这个药的话感冒就不会好,吃不吃 ?
答:吃副作用可不可以忍受 ?
答:至少本来就没头发可以忍上面的副作用有些夸大其词了,但是药物一般来说都会有一些副作用。
那么话说回来,程序中呢?
在I/O模型中,我们希望在在I到O之间只有计算,如果中间包含且不仅包含触发了其他I/O、与此次I -> O计算并不相关的任何事情,都称为副作用。
为什么称之为副作用这样的词语呢,“副作用”这个单词给人第一感觉是糟糕的,从而想让你警惕起来。如果在I/O之间发生了一些我们不知道的副作用,那么我们将无法控制住这个过程,测试过程也会变得非常复杂。
可以想像,如果在I/O之间如果要访问数据库,则必须确保数据库正在运行。如果要写入文件,则必须确保该文件存在并已打开。所以会导致执行过程和测试过程变得很复杂,并不是单一的点对点。
写到这里让我不禁想起了PromiseA+规范的测试用例,官方提供了872种测试用例,你所实现的Promise必须全部通过872种测试用例才符合官方规范。
如果一个I/O模型之间没有副作用的话会有什么样的优势呢?我们参照最开始生活角度的那个例子。
如果感冒药换成某东方国家生产的无副作用的药品,在我们感冒的时候,吃感冒药,感冒好了。过程中无任何副作用的产生,不会秃顶。
那么我们就可以放心的在感冒的情况下吃这种药品,而不用考虑其他情况。这就是一个纯的I/O模型。
在编程中,我们声明了一个函数double,如下
在每次输入x = 3的情况下,double返回恒等与6。它不依赖于我们传递它的参数之外的任何东西。
可以想像,在我们调用double的时候,地球上发生着各种各样的事情,如果在调用的瞬间,天上出现了奥特曼,double依然输出6。在固定输入的情况下它是永恒的。
当你写下这个函数之后,你的余生都可以放心使用它,无论上下文如何,它将永久有效。
永恒的东西变化的频率较低,测试起来更加容易,调试起来更加容易。这就是为什么现在很多编程语言都倾向于无副作用。
如果函数有副作用,我们将其称为过程
函数式编程是基于没有副作用的这样一个简单的前提。在这种范例中,副作用是被排斥的。
如果函数有副作用,我们将其称为过程,或者命令式。因此函数没有副作用。我们认为,如果函数修改了可变数据结构或变量,使用I/O,引发异常或中止错误,则将产生副作用。所有这些东西都被认为是副作用。
副作用之所以不好,是因为(如果有)副作用,取决于系统状态,功能可能是不可预测的。当一个函数没有副作用时,我们可以随时执行它,在给定相同的输入的情况下,它将始终返回相同的结果。
但是要声明一点,函数式编程并不是不需要副作用,只是在需要时限制它们。
需要有副作用,因为没有它们,我们的程序将只能进行计算。
我们经常必须写数据库,与外部系统集成或写文件。与外界通过接口的形式交互才能将我们的计算展示出去。所以很多倾向无副作用的语言的中心细想是把“作用”与“副作用”分离开来处理。
下面我们通过一些特性来看一下。
对于同一输入总是返回相同结果的函数称为纯函数。因此,纯函数是没有可观察到的副作用的函数,如果函数有任何副作用,即使我们使用相同的参数调用它,也可能返回不同的结果。所以我们可以将纯函数替换为其计算值,例如:
如果我们输入x = 2, y = 2,那么我们可以得到 4 = sum(2, 2)。
那么sum为纯函数吗?很显然是的,如果我们恒定传入x = 2, y = 2。那么sum将恒定输出4.
那么意味着 f(2, 2) 可以替换为4,比如 Math.floor(sum(2, 2)) 替换之后 Math.floor(4),是一致的。
它就像一个很大的查询表。我们可以这样做是因为它没有任何副作用。用其计算值替换表达式的能力称为参照透明性。
引用透明很重要,因为它允许我们用值替换表达式。此属性使我们能够使用替换模型来思考和推理程序评估。因此,可以说可以用值替换的表达式是确定性的,因为它们始终为给定的输入返回相同的值。
在讲局部副作用之前,我们先来举一个非局部副作用的典型例子。
上述的factorial函数有副作用吗?
答:很显然是有的。函数内部与外界产生了可见的交互,外界result值在函数内部被修改了。而且第一次调用factorial(2) 返回值为 3,第二次调用返回值为6。对于统一输入不能总返回同一结果。这种副作用是被函数式编程思想所排斥的,与外界的交互使得factorial具有不确定性。
接下来我们看一下另一个例子
那么问题来了,这次factorial有副作用吗?
答:有副作用,因为for每次执行的时候都会改变factorial的返回值,result在不断改变。
但是即使这样,factorial(2) 也可以用一个值代替,如果把factorial看作一个黑盒子,从外部我们是看不到副作用的。每次的输入x = 2,总会有固定的返回值3。
换句话说,该函数是具有确定性的,我们说的功能有局部副作用,但此功能的用户并不关心,因为它没有破坏我们的替代模式。因此,即使具有局部副作用,该函数也是纯净的。这也是上面为什么说“产生了可见的交互”,很显然这句话就是这么严谨,如果见不到,依然是纯的。
在函数式编程开发中,可以用一些技巧,比如利用容器,把一些副作用控制在局部以达到纯的目的。
举一些副作用的典型例子
想了想还是在这里立举一些典型有副作用的例子,通过例子可以更好的理解这种思想。