副作用是好事吗?

问题描述 投票:27回答:13

我觉得这个词很贬义。因此,我对维基百科中的两句话感到惊讶:

已知命令性编程用于使用副作用来使程序起作用。反过来,功能编程因其副作用的最小化而闻名。 [1]

由于我有点数学偏见,后者听起来很棒。副作用的论据是什么?它们是指失去控制还是接受不确定性?他们是好事吗?

functional-programming procedural-programming side-effects
13个回答
51
投票

我经常看到一个关于SO的问题迫使我花半个小时编辑一篇非常糟糕的维基百科文章。这篇文章现在只是中等不好。在关于你的问题的部分,我写了如下:

在计算机科学中,如果除了产生一个值之外,它还修改某个状态或者与调用函数或外部世界具有可观察的交互,则称函数或表达具有副作用。例如,函数可能会修改全局变量或静态变量,修改其中一个参数,引发异常,将数据写入显示或文件,读取数据,调用其他副作用函数或启动导弹。在存在副作用的情况下,程序的行为取决于过去的历史;也就是说,评估的顺序很重要。因为理解有效的程序需要考虑所有可能的历史,副作用通常会使程序更难理解。

副作用对于使程序与外部世界(人,文件系统,网络上的其他计算机)进行交互至关重要。但是副作用的使用程度取决于编程范式。对于不受控制的混杂使用副作用,已知命令式编程。在函数式编程中,很少使用副作用。标准ML和Scheme等功能语言不会限制副作用,但程序员习惯于避免它们。功能语言Haskell使用静态类型系统限制副作用;只有产生IO类型结果的函数才会产生副作用。


1
投票

没有副作用,您无法执行I / O操作;所以你不能做一个有用的应用程序。


1
投票

由于你的程序必须有副作用才能产生任何输出或有趣的效果(除了加热你的CPU),问题是在你的程序中应该触发这些副作用的地方。如果它们隐藏在你不期望它们的方法中,它们就变得有害。

根据经验:将纯方法和方法与副作用分开。将某些内容输出到控制台的方法应该只执行该操作,而不是计算您可能希望在其他位置使用的某些有趣值。


0
投票

嗯,一方面,使用副作用进行编程会更容易,也更直观。功能性编程对于很多人来说很难解决 - 找到一个在Ocaml上教授/ TAed课程的人,你可能会得到关于人们很难理解它的各种故事。如果没人能真正遵循它,那么设计精美,效果好的免费功能代码有什么用呢?雇用人员使您的软件完成相当困难。

这至少是争论的一个方面。有许多原因导致许多人需要学习功能风格,无副作用的代码。想到多线程。


0
投票

那句话真让我笑了起来。也就是说,我发现副作用的最小化真正转化为更容易推理和维护的代码。但是,我没有像我想的那样探索函数式编程的奢侈。

我在研究围绕副作用的面向对象语言和程序语言时看待它的方式是包含和隔离副作用。

作为基本示例,视频游戏具有将图形渲染到屏幕的必要副作用。然而,关于副作用,这里有两种不同的设计路径。

人们试图通过使渲染器非常抽象并且基本上告诉要呈现什么来最小化和放松耦合。系统的其他部分然后告诉渲染器要绘制什么,这可能是一组原始像三角形和带有投影和模型视图矩阵的点,或者可能是更高级别的东西,如抽象模型和相机以及灯光和粒子。无论哪种方式,这样的设计都围绕着导致外部副作用的许多事情,因为代码库的许多部分可能会将更改推送到渲染器(无论多么抽象或间接,净效果仍然是这样的一大堆事物。系统触发外部渲染副作用)。

另一种方法是包含/隔离这些副作用。它不是告诉渲染器渲染什么,而是与游戏世界耦合(尽管这可能只是一些基本的抽象,也许可以访问场景图)。现在它自己访问场景(只读访问)并浏览场景并使用更多的拉式设计找出要渲染的内容。这导致从渲染器到游戏世界的更多耦合,但这也意味着与屏幕输出相关的副作用现在完全包含在渲染器内。

后一种设计包含或隔离副作用,我发现这种类型的设计更容易维护和保持正确。它仍然会导致副作用,但是与将图形输出到屏幕相关的所有副作用现在都完全包含在渲染器中。如果那里存在问题,您就会知道错误将出现在渲染器代码中,而不是外部滥用它并告诉它错误操作的结果。

因此,当涉及到耦合时,我总是发现在引起外部副作用和最小化传入(进入)耦合的事物中最大化传出(传出)耦合更为可取。无论抽象如何,这都适用。在副作用的背景下,对IRenderer的依赖仍然是对具体的Renderer的依赖,只要沟通涉及将要发生的副作用。就具体的副作用而言,抽象没有区别。

渲染器应该依赖于世界其他地方,以便它可以将这些副作用完全隔离到屏幕上;世界其他地方不应该依赖渲染器。对文件保护程序来说也是类似的。不应该告诉文件保护程序外面的世界要保存什么。它应该关注它周围的世界,并找出自己要节省的东西。这将是寻求隔离和遏制副作用的设计路径;它倾向于比基于推的更基于拉动。如果你绘制出依赖关系,结果往往会引入更多的耦合(虽然它可能是松散的),因为saver可能需要与它甚至不想保存的事物耦合,或者渲染器可能需要只读对事物的访问它甚至没有兴趣渲染发现它对渲染感兴趣的东西。

然而,最终结果是依赖性远离副作用而不是副作用。当我们有一个具有许多依赖性的系统流向推动外部副作用时,我总是找到那些最难推理的系统,因为系统的这么多部分可能会改变外部状态,直到不仅难以弄清楚是什么将要发生,但也发生在何时何地。因此,纠正/防止该问题的最直接的方法是寻求使依赖性从副作用流出,而不是朝向它们。

无论如何,我发现偏爱这些类型的设计是实用的方法,以帮助避免错误,并帮助检测和隔离它们,以使它们更容易重现和纠正。

我发现的另一个有用的策略是使任何给定的系统循环/阶段的副作用更均匀。例如,我没有做一个从某个东西中删除相关数据的循环,将它脱钩然后将其删除,我发现在这种情况下你做三个齐次循环会更容易。第一个同构循环可以删除相关数据。第二个同构循环可以使节点脱链。第三个同质环可以将其从系统的其余部分中移除。这是一个较低级别的注释,更多地与实现相关而不是设计,但我经常发现结果更容易推理,维护,甚至优化(更容易并行化,例如,并且具有改进的参考局部性) - 你采取那些非均匀的循环触发多种不同类型的副作用并将它们分解成多个均匀的循环,每个循环触发一种统一的副作用。


25
投票

副作用是必要的邪恶,人们应该寻求最小化/本地化它们。

关于线程的其他评论说,无效编程有时不那么直观,但我认为人们认为“直观”的主要原因在于他们以前的经验,而且大多数人的经历都有严重的命令性偏见。主流工具每天都在变得越来越实用,因为人们发现无效编程导致更少的错误(尽管有时候肯定是新的/不同类型的错误),因为单独的组件通过效果进行交互的可能性较小。

几乎没有人提到性能,并且无效编程通常具有比有效性更差的性能,因为计算机是von-Neumann机器,其设计用于效果良好(而不是设计为与lambda一起工作)。现在我们正处于多核革命之中,这可能会改变游戏,因为人们发现他们需要利用核心获得性能,而并行化有时需要火箭科学家来充分发挥作用,当你没有效果时,很容易就能做到。


17
投票

在von-Neumann机器中,副作用是使机器工作的东西。从本质上讲,无论你如何编写程序,它都需要做副作用才能工作(在低级视图中)。

没有副作用的编程意味着抽象出副作用,这样你就可以一般地思考问题 - 而不用担心机器的当前状态 - 并减少程序的不同模块之间的依赖关系(无论是程序,类还是其他任何东西)。通过这样做,您将使您的程序更可重用(因为模块不依赖于特定的状态来工作)。

所以是的,副作用免费程序是一件好事,但副作用在某种程度上是不可避免的(因此它们不能被视为“坏”)。


9
投票

优点:

  • 最后,副作用是你想要完成的。
  • 对于与外界交互的代码,副作用很自然。
  • 他们使许多算法变得简单。
  • 为避免使用副作用,您需要通过递归实现循环,因此您的语言实现需要尾调用优化。

有:

  • 纯代码易于并行化。
  • 副作用会使代码变得复杂。
  • 纯代码更容易证明是正确的。

例如Haskell,起初看起来非常优雅,但是你需要开始玩外面的世界,它不再那么有趣了。 (Haskell将状态作为函数参数移动并将其隐藏在称为Monads的东西中,这使您能够以命令式的外观类型进行编写。)


7
投票

没有副作用,你根本无法做某些事情。一个例子是I / O,因为根据定义,在屏幕上显示消息是副作用。这就是为什么函数式编程的目标是尽量减少副作用,而不是完全消除它们。

除此之外,通常会将副作用最小化与其他目标冲突,例如速度或内存效率。其他时候,已经存在一个问题的概念模型,该模型与变异状态的想法很好地吻合,并且与现有模型作斗争可能会浪费精力和精力。


7
投票

副作用就像任何其他武器一样。它们毫无疑问是有用的,并且在处理不当时可能非常危险。

像武器一样,你有各种不同程度杀伤力的副作用。

在C ++中,由于指针,副作用完全不受限制。如果变量声明为“私有”,您仍然可以使用指针技巧访问或更改它。您甚至可以更改不在范围内的变量,例如调用函数的参数和本地。在OS(mmap)的帮助下,您甚至可以在运行时修改程序的机器代码!当你用C ++这样的语言写作时,你就会被提升到Bit God的等级,掌握你进程中的所有记忆。编译器对代码进行的所有优化都是在假设您不滥用权限的情况下进行的。

在Java中,您的能力受到更多限制。范围内的所有变量都在您的控制之下,包括由不同线程共享的变量,但您必须始终遵守类型系统。尽管如此,由于操作系统的一部分可供您使用并且存在静态字段,您的代码可能具有非本地效果。如果一个单独的线程以某种方式关闭System.out,它看起来就像魔术一样。它将是魔术:副作用魔法。

Haskell(尽管有关于纯粹的宣传)有IO monad,它要求你用类型系统注册你所有的副作用。将你的代码包装在IO monad中就像是手枪的3天等待时间:你仍然可以自己踩脚,但直到你和政府合作才行。还有不安全的PerformIO及其同类产品,这是Haskell IO的黑市,给你带来“无问题”的副作用。

米兰达是Haskell的前身,是一种纯粹的功能性语言,在monads开始流行之前就已经创建了。米兰达(据我所知......如果我错了,替代Lambda Calculus)根本没有IO基元。唯一完成的IO是编译程序(输入)并运行程序并打印结果(输出)。在这里,你有完全的纯洁。执行顺序完全无关紧要。所有“效果”都是声明它们的函数的局部,意味着两个不相交的代码部分永远不会相互影响。这是一个乌托邦(对于数学家而言)。或者等同于一种歪曲。这很无聊。什么都没发生。你不能为它编写服务器。你不能在其中编写操作系统。你不能在其中写入SNAKE或俄罗斯方块。每个人都只是坐着看数学。


6
投票

陪审团还在外面。自计算机开始以来,审判一直在进行,所以不要期待很快就能做出判决。


3
投票

副作用对于大多数应用的重要部分是必不可少的。纯功能有很多优点。它们更容易思考,因为您不必担心前后条件。由于它们不会改变状态,因此它们更容易并行化,随着处理器数量的增加,这将变得非常重要。

副作用是不可避免的。并且只要它们是更复杂但纯粹的解决方案,它们应该被用于更好的选择。纯函数也是如此。有时,使用功能性解决方案可以更好地解决问题。

这一切都很好=)你应该根据你正在解决的问题使用不同的范例。


3
投票

正如有些人在这里提到的那样,没有副作用就无法做出有用的应用。但是从那以后,并不是说以不受控制的方式使用副作用是一件好事。

请考虑以下类比:具有没有分支指令的指令集的处理器绝对没有价值。然而,并不是说程序员必须一直使用gotos。相反,事实证明结构化编程和后来的像OOP这样的OOP语言甚至没有goto语句就可以做到,没有人错过它。

(可以肯定的是,Java中仍有goto - 它现在称为break,continue和throw。)

© www.soinside.com 2019 - 2024. All rights reserved.