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


69

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

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


6
没有副作用的程序是没有用的,因此副作用既不是邪恶的也不是禁忌。但是FP强制使用副作用来分隔代码,因此,尽可能多的代码部分是无副作用的函数。鼓励这样做是因为无副作用的功能和子系统更易于理解,易于分析,易于测试和易于优化。
JacquesB

@JacquesB它将是一个很好的答案,以解释为什么它们更易于理解,易于分析,易于测试和易于优化。
ceving

Answers:


72

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

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

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

编辑:在Benjol的请求:因为你的很多国家的保存在栈(数据流,不控制流程,乔纳斯把它称为在这里),你可以parallelise或以其他方式重新排序独立于你的计算的那些部分的执行彼此。您可以轻松找到那些独立的部分,因为一个部分不提供输入。

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


4
也许考虑在您的答案中添加一些关于并发的内容?
Benjol 2010年

5
无副作用的功能更易于测试和重用。
LennyProgrammers

@ Lenny222:重用是我在谈论函数组成时所暗示的。
Frank Shearar

@弗兰克:啊,好的,浏览太浅了。:)
LennyProgrammers

@ Lenny222:没关系;阐明它可能是一件好事。
Frank Shearar

23

从有关函数式编程的文章中:

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



23

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

从本质上讲,我在说什么是功能程序员,认为副作用不是有害的,他们只是认为限制使用副作用是件好事。我知道这看起来像是一个简单的区分,但一切都与众不同。


这就是为什么它们“有些忌讳”的原因-FPL鼓励您限制副作用。
Frank Shearar

+1。副作用仍然存在。实际上,它们是有限的
Belun

为了澄清起见,我没有说过“为什么在函数式编程中不允许副作用”或“为什么不需要副作用”。我知道功能语言允许这样做,有时是必须的。但是在函数式编程中不建议这样做。为什么?那是我的问题。
Gulshan

@Gulshan-因为副作用使程序更难于理解和优化。
2010年

对于haskell来说,重点不是“限制副作用”。副作用无法用LANGUAGE表达。诸如此类readFile的功能是定义一系列动作。该序列在功能上是纯净的,有点像描述要做什么的抽象树。然后由运行时执行实际的脏副作用。
萨拉

13

一些注意事项:

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

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


非常有趣的一点:函数是否真的执行并不重要。这将是有趣的与可以摆脱后续调用给出等效参数无副作用功能的编译器来结束。
诺埃尔·威德默

1
@NoelWidmer类似的东西已经存在。Oracle的PL / SQL deterministic为没有副作用的函数提供了一个子句,因此它们执行的次数不会超过必要的次数。
user281377

哇!但是,我认为语言应具有语义上的表达,以便编译器可以自行解决它,而不必指定显式标志(我不确定子句是什么)。解决方案可能是将参数指定为可变/不可变的。一般来说,这将需要一个强大的类型系统,在该系统中,编译器可以对副作用进行假设。并且如果需要,该功能将需要能够被关闭。选择退出而不是选择加入。基于我自阅读您的回答以来所掌握的有限知识,这只是我的意见:)
Noel Widmer

deterministic子句只是一个关键字,它告诉编译器这是确定性函数,与finalJava中的关键字告诉编译器变量不能更改的方式类似。
user281377

11

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

考虑以下简单示例:

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我甚至不须在之间的代码,更无法理解,或者看看它调用函数的实现。

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


6

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

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

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

在一个很高的层次上,想象一下仅用黑盒测试就可以测试整个3层网站。当然,这是可行的,具体取决于规模。但是肯定有很多重复。如果有一个bug(即相关的副作用),那么你就可能打破整个系统进行进一步的测试,直到错误的诊断和修复,以及修复被部署到测试环境。

好处

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

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

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


1
Ada很难引起副作用。虽然这并非不可能,但是您清楚地知道自己该怎么做。
mouviciel 2010年

@mouviciel:我认为至少有一些有用的语言使副作用变得非常困难,并尝试将它们放归Monads。
Merlyn Morgan-Graham 2010年

4

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

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

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


+1-“或一些毫无戒心的同事”
Merlyn Morgan-Graham 2010年

1
副作用为-1表示“需要处理的泄漏”。创建“副作用”(非纯功能代码)是编写任何平凡的计算机程序的全部目的。
梅森惠勒

六年后发表了这一评论,但有副作用,然后又有副作用。对于任何程序来说,确实需要一种理想的副作用,例如进行I / O等,因为您需要以某种方式将结果提供给用户-但是另一种副作用,即您的代码在没有良好状态的情况下更改状态诸如执行I / O之类的原因,确实是“泄漏”,需要稍后处理。基本思想是命令查询分离:返回值的函数不应有副作用。
rmunn17年

4

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

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

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

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

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


有趣的是,逻辑编程不仅没有功能上的副作用;但是一旦分配,您甚至无法更改变量的值。
伊兰2010年

@Ilan:对于某些功能语言也是如此,它是一种易于采用的样式。
back2dos

“函数式编程采用了一种更为激进的方法,从程序员的角度来看,应用程序状态只是不可变的。这是一个不错的主意,但是却使语言本身无用。为什么?因为任何I / O操作都有其优势效果”:FP不禁止副作用,而是在不需要时限制它们。例如(1)I / O->副作用是必要的;(2)不需要从值序列->副作用(例如,带累加器变量的循环)中计算集合函数。
乔治

2

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

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

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


1

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

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

这些修改堆和修改屏幕的副作用的正确排列绝不是邪恶,而是OO设计的核心(在本例中为MVC模式)。

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


1
观察者(包括UI)应该通过订阅对象发出的消息/事件来了解有关修改的信息。除非对象直接修改观察者,否则这不是副作用,这将是不良的设计。
克里斯·弗雷德

1
@ChrisF最肯定的是副作用。传递给观察者的消息(在OO语言中,很可能是在接口上进行方法调用)将导致堆上UI组件的状态发生更改(并且这些堆对象对程序的其他部分可见)。UI组件既不是方法的参数也不是返回值。从形式上讲,要使功能无副作用,它必须是幂等的。MVC模式中的通知不是,例如,UI可能会显示它已收到的消息列表-控制台-两次调用它会导致程序状态不同。
flamingpenguin 2010年

0

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

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


0

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

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

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

0

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

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

但是,我认为,或者至少是尝试,我是一个务实的人,而且我认为我们不必为了使我们的代码正确(至少我会发现使用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)。但是最重​​要的是,我发现第二个版本在正确性和更改方面都更容易做出推理,而不会引起错误。以便'

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


-1

这不是邪恶的。我认为,有必要区分两种功能类型-有副作用和没有副作用。没有副作用的函数:-带有相同参数的返回总是相同的,因此例如没有任何参数的函数是没有意义的。-这也意味着,某些这样的函数所调用的顺序不起作用-必须能够运行并且可以单独调试(!),而无需任何其他代码。现在,大声笑,看看JUnit是什么。具有副作用的功能:-具有“泄漏”,可以自动突出显示-通过调试和查找错误(通常是由副作用引起的错误)非常重要。-任何具有副作用的功能本身也具有“部分”,而没有副作用,这些功能也可以自动分离。那些副作用是如此邪恶,


这似乎没有提供任何实质性的内容来说明和解释先前的12个答案
gnat 2016年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.