it-swarm.cn

为什么在函数式编程中副作用被认为是邪恶的?

我觉得副作用是自然现象。但这在功能语言中有点像忌讳。是什么原因

我的问题特定于函数式编程风格。并非所有的编程语言/范例。

70
Gulshan

编写没有副作用的函数/方法-因此它们是纯粹的函数-使推理程序的正确性变得更加容易。

它还使组合这些功能以创建新行为变得容易。

它还使某些优化成为可能,例如,编译器可以存储函数的结果或使用Common Subexpression Elimination。

编辑:应Benjol的要求:由于状态的很多状态都存储在堆栈中(数据流,而不是控制流,如乔纳斯所说的 here ),因此您可以并行化或以其他方式重新排序这些部分的执行彼此独立的计算您可以轻松找到那些独立的部分,因为一个部分不提供输入。

在带有调试器的环境中,该调试器使您可以回滚堆栈并继续计算(例如Smalltalk),拥有纯函数意味着您可以很容易地看到值的变化,因为以前的状态可供检查。在大量变异的计算中,除非您明确将do/undo操作添加到结构或算法中,否则您将看不到计算的历史记录。 (这与第一段有关:编写纯函数使检查程序的正确性更加容易。)

73
Frank Shearar

您错了,函数式编程会限制副作用,从而使程序易于理解和优化。甚至Haskell都允许您写入文件。

实质上,我要说的是函数程序员并不认为副作用是有害的,他们只是认为限制使用副作用是件好事。我知道这看起来像是一个简单的区分,但一切都与众不同。

24
ChaosPandion

摘自有关 函数式编程 的文章:

实际上,应用程序需要具有一些副作用。函数式编程语言Haskell的主要贡献者西蒙·佩顿·琼斯(Simon Peyton-Jones)说:“最后,任何程序都必须操纵状态。没有任何副作用的程序就是一种黑匣子。您所能看到的就是盒子变热了。” ( http://oscon.blip.tv/file/324976 )关键是限制副作用,清楚地识别它们,并避免在整个代码中分散它们。

23
Peter Stuifzand

一些注意事项:

  • 没有副作用的功能可以并行执行,而有副作用的功能通常需要某种同步。

  • 没有副作用的函数可以进行更积极的优化(例如,透明地使用结果缓存),因为只要我们获得正确的结果,函数是否真的甚至都没有关系。被执行

13
user281377

我现在主要从事功能代码的研究,从这个角度来看,它似乎是显而易见的。副作用给试图阅读和理解代码的程序员造成了巨大的精神负担。在一段时间内没有负担之前,您不会注意到该负担,然后突然不得不再次读取具有副作用的代码。

考虑以下简单示例:

val foo = 42
// Several lines of code you don't really care about, but that contain a
// lot of function calls that use foo and may or may not change its value
// by side effect.

// Code you are troubleshooting
// What's the expected value of foo here?

用一种功能语言,我知道foo仍然是42。我什至不必看看之间的代码,更不用说理解它了,或者看看它调用的函数的实现。

关于并发,并行化和优化的所有内容都很不错,但这就是计算机科学家在手册中所写的内容。不必怀疑是谁在变异您的变量,什么时候是我在日常练习中真正享受的。

11
Karl Bielefeldt

几乎没有语言会导致副作用。完全无副作用的语言将很难使用(几乎不可能),除非功能非常有限。

为什么副作用被认为是邪恶的?

因为它们使人们很难对程序的确切功能进行推理,并且很难证明程序可以执行您期望的操作。

在一个很高的层次上,想象一下仅用黑盒测试就可以测试整个3层网站。当然,这是可行的,具体取决于规模。但是肯定有很多重复。并且,如果存在is错误(与副作用相关),那么您可能会破坏整个系统以进行进一步测试,直到诊断出该错误并将其修复,然后将该修复程序部署到测试环境。

好处

现在,将其缩小。如果您相当擅长编写副作用免费代码,那么在推理某些现有代码的作用时,您的速度会提高多少?您编写单元测试的速度可以快多少?您会如何确信没有副作用的代码可以保证没有错误,并且用户可以限制他们接触did所存在的任何错误?

如果代码没有副作用,则编译器可能还会执行其他优化。实施这些优化可能要容易得多。甚至可以概念化无副作用代码的优化,这可能要容易得多,这意味着您的编译器供应商可能会实现在具有副作用的代码中难以实现的优化。

当代码没有副作用时,并发也非常容易实现,自动生成和优化。这是因为所有部分都可以按任何顺序安全地评估。允许程序员编写高度并发的代码被广泛认为是计算机科学需要解决的下一个重大挑战,也是对 摩尔定律 的少数对冲措施之一。

6
Merlyn Morgan-Graham

副作用就像代码中的“泄漏”,以后需要由您或一些毫无戒心的同事来处理。

功能语言避免使用状态变量和可变数据,这是使代码减少上下文依赖和模块化程度的一种方式。模块化确保一个开发人员的工作不会影响/破坏另一个开发人员的工作。

随团队规模扩展开发速度,是当今软件开发的“圣杯”。与其他程序员一起工作时,没有什么比模块性重要。即使是最简单的逻辑副作用也使协作变得极为困难。

4
Ami

好吧,恕我直言,这是很虚伪的。没有人喜欢副作用,但每个人都需要它们。

副作用如此危险,是如果您调用一个函数,那么这不仅会影响该函数在下次调用时的行为方式,而且可能还会影响其他函数。因此副作用带来了不可预测的行为和非平凡的依赖性。

诸如OO之类的编程范例和函数均解决了此问题。 OO通过强加关注点来减少问题。这意味着将包含许多可变数据的应用程序状态封装到对象中,每个对象仅负责维护自己的状态。这样,减少了依赖的风险,问题更加隔离,更易于跟踪。

函数式编程采用了一种更为激进的方法,即从程序员的角度来看,应用程序状态是完全不变的。这是一个不错的主意,但使该语言本身无用。为什么?因为任何I/O操作都有副作用。从任何输入流中读取数据后,应用程序状态可能会发生变化,因为下次调用同一函数时,结果可能会有所不同。您可能正在读取其他数据,或者(也有可能)操作可能失败。输出也是如此。均匀输出是有副作用的操作。如今,您通常没有意识到这一点,但可以想象您只有20K的输出,如果再输出,由于磁盘空间不足或其他原因,应用程序将崩溃。

所以是的,从程序员的角度来看,副作用是令人讨厌和危险的。大多数错误来自应用程序状态的某些部分以几乎模糊的方式互锁的方式,这种方式是通过未考虑到的,并且常常是不必要的副作用而造成的。从用户的角度来看,副作用是使用计算机的重点。他们不在乎内部发生什么或如何组织。他们做了一些事情,并期望计算机会相应地进行更改。

4
back2dos

任何副作用都会引入额外的输入/输出参数,在测试时必须考虑这些参数。

这使得代码验证变得更加复杂,因为环境不仅限于验证的代码,还必须引入周围的部分或全部环境(更新的全局代码位于该代码所在的环境中,而该全局代码又取决于该环境)。代码,而这又取决于驻留在完整的Java EE服务器...中。)。

通过尝试避免副作用,可以限制运行代码所需的外部性。

2
user1249

以我的经验,良好的面向对象编程设计要求使用具有副作用的功能。

例如,以一个基本的UI桌面应用程序为例。我可能正在运行的程序在其堆上具有一个对象图,该对象图表示我的程序域模型的当前状态。消息到达该图中的对象(例如,通过从UI层控制器调用的方法调用)。响应于消息,修改堆上的对象图(域模型)。通知模型观察者任何更改,修改UI以及其他资源。

这些修改堆和修改屏幕的副作用的正确安排绝不是邪恶的,它是OO设计)(在这种情况下为MVC模式)的核心。

当然,这并不意味着您的方法应具有矫揉造作的副作用。而且,无副作用功能确实可以提高代码的可读性,有时甚至可以提高性能。

1
flamingpenguin

正如上面的问题所指出的那样,函数式语言并没有那么多防止代码具有副作用,而是为我们提供了管理哪些副作用可以的工具。在给定的代码段中以及何时发生。

事实证明,这将产生非常有趣的结果。首先,也是最明显的是,您已经可以使用副作用免费代码进行很多操作。但是,即使使用确实有副作用的代码,我们也可以做其他事情:

  • 在具有可变状态的代码中,我们可以通过以下方式管理状态范围:静态地确保状态不会泄漏到给定的函数之外,这使我们能够收集垃圾,而无需引用计数或标记扫掠样式方案,但仍要确保没有任何引用可以保留。相同的保证对于维护对隐私敏感的信息等也很有用。(可以使用haskell中的ST monad来实现)
  • 当在多个线程中修改共享状态时,我们可以通过跟踪更改并在事务结束时执行原子更新,或回滚事务并在另一个线程进行有冲突的修改时重复执行该事务来避免锁定。这仅是可以实现的,因为我们可以确保代码除了状态修改(我们可以很乐意放弃)之外没有其他影响。这是由Haskell中的STM(软件事务存储)单子执行的。
  • 我们可以跟踪代码的效果并将其沙沙化,过滤它可能需要执行的所有效果以确保它是安全的,从而允许(例如) 用户输入的代码可以在网站上安全地执行
0
Jules

邪恶有点过头了。这全都取决于语言用法的上下文。

对那些已经提到的问题的另一个考虑是,如果没有功能上的副作用,它会使程序正确性的证明变得更加简单。

0
Ilan

在复杂的代码库中,副作用的复杂交互是我发现要推理的最困难的事情。鉴于我的大脑运作方式,我只能亲自讲话。副作用和持续状态以及变异的输入等等使我不得不思考事情的“何时”和“何处”是正确性的原因,而不仅仅是每个功能中都发生了“什么”。

我不能只专注于“什么”。在彻底测试了一个会引起副作用的函数之后,我无法得出结论,因为它将在使用该代码的整个代码中散布可靠性,因为调用者可能仍会通过在错误的时间,错误的线程,错误的位置调用它来滥用它。订购。同时,没有副作用,仅在给定输入(不触摸输入)的情况下返回新输出的函数,这种方式几乎是不可能被滥用的。

但是,我认为,或者至少是尝试,我是一个务实的人,而且我认为我们不必为了使我们的代码正确(至少我会发现用C)这样的语言很难做到这一点。我发现很难推理出正确性的地方是我们将复杂的控制流程和副作用结合在一起。

对我来说,复杂的控制流本质上是类似于图的,通常是递归或递归的(例如事件队列,例如,它们不直接递归地调用事件,而本质上是“递归式”),也许在做在遍历实际的链接图结构或处理包含事件的折衷混合的非同质事件队列的过程中,将导致我们到达代码库的各种不同部分,并触发不同的副作用。如果您试图绘制出最终将在代码中最终出现的所有位置,它将类似于一个复杂的图形,并且可能在该给定的时刻,并且在所有给定的条件下,您都不会想到图形中的节点引起副作用,这意味着您可能不仅会对所调用的功能感到惊讶,而且还可能对这段时间内发生的副作用以及发生的顺序感到惊讶。

函数式语言可能具有极其复杂和递归的控制流,但结果在正确性方面如此容易理解,因为在此过程中并没有发生各种折衷的副作用。只有当复杂的控制流遇到折衷的副作用时,我才感到头疼,试图理解正在发生的全部事情,以及它是否总是会做正确的事情。

因此,在遇到这种情况时,我常常会感到很难(即使不是不可能)对此类代码的正确性充满信心,更不用说非常有信心我可以对此类代码进行更改而不会遇到意外情况。因此,对我来说,解决方案要么是简化控制流程,要么是最小化/统一副作用(通过统一,我的意思是像在系统的特定阶段仅对多种事物造成一种类型的副作用,而不是两个或三个或一个)。打)。我需要发生以下两件事之一,以使我的简单大脑对现有代码的正确性和所引入的更改的正确性充满信心。如果副作用与控制流一致且简单,那么很容易对引入副作用的代码的正确性充满信心,例如:

for each pixel in an image:
    make it red

这样的代码的正确性很容易推断出来,但是主要是因为副作用非常统一并且控制流程是如此简单。但是,我们有这样的代码:

for each vertex to remove in a mesh:
     start removing vertex from connected edges():
         start removing connected edges from connected faces():
             rebuild connected faces excluding edges to remove():
                  if face has less than 3 edges:
                       remove face
             remove Edge
         remove vertex

然后,这是过于简单的伪代码,通常将涉及更多的函数和嵌套循环以及将要进行的更多操作(更新多个纹理贴图,骨骼权重,选择状态等),但是即使伪代码也使得它很难复杂图状控制流与副作用的相互作用导致正确性的原因。因此,一种简化的策略是推迟处理,并且一次只关注一种副作用:

for each vertex to remove:
     mark connected edges
for each marked Edge:
     mark connected faces
for each marked face:
     remove marked edges from face
     if num_edges < 3:
          remove face

for each marked Edge:
     remove Edge
for each vertex to remove:
     remove vertex

...简化后的迭代效果。这意味着我们要多次传递数据,这肯定会带来计算成本,但是由于副作用和控制流程具有这种统一且更简单的性质,因此我们经常发现可以更轻松地对此类结果代码进行多线程处理。此外,与遍历连接的图并在运行时产生副作用相比,可使每个循环更友好地缓存(例如:使用并行位集来标记需要遍历的内容,以便我们可以按排序的顺序进行延迟的遍历使用位掩码和FFS)。但最重要的是,我发现第二个版本在正确性和更改方面都比较容易推理,而不会引起错误。因此,无论如何我都是这样,并且我采用了相同的思维方式来简化上面的网格处理,从而简化了事件处理等工作-更加复杂的循环以及死掉的简单控制流导致了统一的副作用。

毕竟,我们需要在某个时候发生副作用,否则我们将只拥有无处可去的输出数据功能。通常,我们需要将某些东西记录到文件中,将某些东西显示在屏幕上,通过套接字将数据发送出去,这种东西,而所有这些都是副作用。但是,我们绝对可以减少发生的多余副作用,并且还可以减少当控制流非常复杂时发生的副作用的数量,而且我认为如果这样做的话,避免错误会容易得多。

0
user204677