你需要知道的有关泛函编程一些知识 —— effect 和 side effect

在翻译 ZIO 文档的时候有一个比较头疼的问题是有关如何翻译 effect 和 side effect。在许多文章中 effect 有时被翻译成“作用”,而有的时候又被翻译成“效果”。side effect 则在多数时候被翻译成“副作用”,很少见到被翻译成“副效果”的,因为中文中似乎没有这样一个词汇。当我第一次看到“副作用”这个词的时候的第一反应是该吃药了。但是不管怎么翻译,这都很容易产生理解上的偏差,甚至有时不管用哪个词都不正确。所以为了更准确地表达原文,最后我决定还是尽可能不对这两个词做翻译,保持它们的英文原文。其实只要准确理解了 effect 和 side effect 的内涵并不影响阅读,就当是两个专有名词好了。

effect 和 side effect 是一对反义词(貌似废话),只要理解了一个,另一个自然也就理解了。他俩的关键在如何理解什么是 side effect。简单地说,side effect 指的是那些任何主动或被动地对“非当前局部环境变量”的访问的操作。

注意,“非当前局部环境变量”不仅仅包括访问全局变量和静态变量。这个操作可能非常广泛,甚至不局限于变量,还包括访问外部 IO,执行某些特殊指令,以非正常返回的方式引起的主动或被动出栈(比如异常)等行为。总之,它包含了大部分的栈外资源,要特别强调的是哪怕对本地变量的操作也可能引发栈外动作,比如除 0 运算,哪怕被除数是一个本地变量,但是因为它会触发异常,这也被视为 side effect。总之一切除了正常返回外,任何可能引入栈外因数的行为,都被称为 side effect。当然,还需要加一个前提是这些行为必须是“可观测”到的,如果你适当地将引起异常的运算用 try…cache…包裹起来并不将异常直接弹出,那么可以被认为消除了由此带来的 side effect。但是这种消除 side effect 的手段非常有限,绝大多数的对当前环境之外的资源的访问都是可以被观测到的,比如对网络的访问,它几乎无法被隐藏。

为什么要将这些操作单独区分出来呢?因为这些操作对程序的正常运行可能产生“危害“,这里的所谓“危害”并不是贬义词,而是指它可能导致函数结果的不可预测(不纯),甚至可能产生运行时中断。

什么叫函数的不纯?想象一下当我们用一张纸来解答一道存粹的数学函数的时候,我们最终得到的答案将完全依赖当前这张纸上的这道函数的输入参数,并且我们也一定会得到一个确定的结论(包括“无解”和那些尚未被数学家解答出来的“猜想”,它们也最终会有一个确定的结论),并且这个结论的改变只依赖如上参数条件。这就是函数的存粹性。函数的纯粹性有一个非常可贵的特点,就是一个存粹的函数不受任何外部宇宙条件的影响,它都会而且只会得到一个仅仅与参数有关的确定答案。无法想象我们换一张纸来答题,或者到火星上就会让 1+1得到一个非2的答案。也就是说,一个函数的存粹性取决于两个必要条件:1)是否所有的求解条件都由参数来传递?2)是否返回一个确定的结论,并且这个结论只和参数相关?

在计算机中满足了以上两个必要条件还不能称为纯函数,它还必须保证求解的过程不产生主动中断。side effect 除了会引入域外因数(外部变量)外,还有一个重要的原因是许多 side effect 的行为都会引起 CPU 在求解的过程中产生让程序脱离执行序列的中断。比如在执行的过程中插入 sleep 指令,这条指令将中断当前任务并将它调离执行序列进入睡眠状态。在CPU的执行过程中有许多类似的主动或被动的中断事件,包括在多线程环境中访问共享变量,设立临界区,这可能导致大量线程陷入等待中断。side effect 主要关注的是那些由程序内部引发的中断,包括但不限于 IO 中断(不包括缺页中断)。因为这些中断会大大地降低代码的执行效率。

除了 side effect 之外剩下的(在概念上非常局限的)那一部分,就称为 effect,也就是泛函编程中极力追求的目标。effect 是保证我们写出纯函数的必要(非充分)条件,而纯函数是泛函编程“高效率”,“低故障”,“可验证”的重要保障,因此我们要非常清楚如何正确地利用或避免 side effect 来构建我们的程序以达到我们希望的目的。

最后要强调一点,明确认识 side effect 并非让我们“惧怕”使用它们,一个程序最终还是要通过 IO 和人或设备打交道,因此一个功能的程序几乎不可避免用到 side effect,认识 side effect 只是让我们能够更好地组织我们的代码,避免在不必要的地方使用,让程序运行的更加高效可靠。

Leave a Reply
Your email address will not be published.
*
*

BACK TO TOP