副作用是好事吗?

时间:2009-04-18 17:49:02

标签: functional-programming procedural-programming side-effects

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

  

命令式编程众所周知   采用副作用来制作   程序功能。实用   反过来编程也因其而闻名   最小化副作用。 [1]

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

14 个答案:

答案 0 :(得分:51)

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

  

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

     

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

答案 1 :(得分:25)

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

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

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

答案 2 :(得分:17)

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

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

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

答案 3 :(得分:9)

临:

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

缺点:

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

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

答案 4 :(得分:7)

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

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

答案 5 :(得分:7)

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

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

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

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

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

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

答案 6 :(得分:6)

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

答案 7 :(得分:3)

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

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

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

答案 8 :(得分:3)

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

考虑以下类比:具有没有分支指令的指令集的处理器绝对没有价值。但是,并不是程序员必须始终使用转到。相反,事实证明,结构化编程和后来的Java语言,如果没有goto语句就可以做到,没有人会错过它。

(可以肯定的是,Java中仍然存在 - 现在称为中断继续抛出。)

答案 9 :(得分:1)

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

答案 10 :(得分:1)

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

根据经验:将纯方法和方法与副作用分开。一个向控制台输出内容的方法应该只做那个而不是计算一些你可能想在其他地方使用的有趣值。

答案 11 :(得分:0)

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

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

答案 12 :(得分:0)

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

当我使用围绕副作用的面向对象和过程语言时,我看待它的方式是包含 isolate 副作用。

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

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

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

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

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

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

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

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

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

答案 13 :(得分:0)

我认为,平衡的答案是,人们应该寻找机会来最大程度地减少或避免副作用,或者考虑它们的位置,并寻找机会将其移至其他位置以使代码更易于理解。

在这里,我给出了一些代码的两个版本。当我开始寻找机会将第一个版本的代码更改为第二个版本的代码时,我的生活变得更好了:

该对象可能具有属性first, second, third和方法first_method, second_method, third_method之类的对象

def first_method(self):
    # calculate and set self.first

def second_method(self):
    # use self.first to calculate and set self.second

def third_method(self):
    # use self.first and self.second to calculate self.third

然后是高级方法:

def method(self):
    self.first_method()
    self.second_method()
    self.third_method()

现在想象一下,这些名称不是以“ first”,“ second”,“ third”为前缀,而是描述方法的作用,以及程序更复杂。在探索滥用副作用的代码时,我通常会遇到这个问题。我经常需要查看这些函数的实现,以了解调用它们的作用以及它们如何协同工作。

现在不用胡说“副作用是邪恶的”了,我们仍然可以从避免副作用的愿望中受益:

def first_func():
    # calculate the first thing and return it

def second_func(first_thing):
    # use first_thing to calculate second_thing and return it

def third_func(first_thing, second_thing):
    # use first_thing and second_thing to calculate third_thing and return it

和更高级别的方法看起来像

def method(self):
    self.first_thing = first_func()
    self.second_thing = second_func(self.first_thing)
    self.third_thing = third_func(self.first_thing, self.second_thing)

我们仍然有副作用,method设置对象的属性,但是当我们试图了解组成它的功能如何协同工作时,很明显,您必须按此顺序调用它们而且每个人都需要做些什么很明确。

在第一个版本中,当查看method的实现时,谁知道对象的其他属性会因每个函数而改变。在查看第二个版本时,很明显所有人都可以看到调用method仅更改三个属性,而不必查看函数的实现。

此示例可能看起来很简单,因为出于解释的原因我是凭空制作的。在尝试理解编写的代码时,我尽力传达了一些实际问题,而这些代码对副作用没有任何轻视。与第一个版本一样运行的真实代码比第二个版本更难以理解。

正如其他人所说,我对副作用的看法是,值得去问问自己是否可以移动或避免副作用,而这样做经常会使代码更容易理解。