Answers:
编写没有副作用的函数/方法-因此它们是纯函数 -使得更容易推理程序的正确性。
这也使组合这些功能以创建新行为变得容易。
它还使某些优化成为可能,例如,编译器可以存储函数的结果或使用Common Subexpression Elimination。
编辑:在Benjol的请求:因为你的很多国家的保存在栈(数据流,不控制流程,乔纳斯把它称为在这里),你可以parallelise或以其他方式重新排序独立于你的计算的那些部分的执行彼此。您可以轻松找到那些独立的部分,因为一个部分不提供输入。
在具有调试器的环境中,该调试器使您可以回滚堆栈并继续计算(例如Smalltalk),拥有纯函数意味着您可以很容易地看到值的变化,因为以前的状态可供检查。在大量变异的计算中,除非您向结构或算法中显式添加do / undo操作,否则您将看不到计算的历史记录。(这与第一段有关:编写纯函数使检查程序的正确性更加容易。)
从有关函数式编程的文章中:
实际上,应用程序需要具有一些副作用。函数式编程语言Haskell的主要贡献者西蒙·佩顿·琼斯(Simon Peyton-Jones)说:“最后,任何程序都必须操纵状态。没有任何副作用的程序就是一种黑匣子。您所能看到的就是盒子变热了。” (http://oscon.blip.tv/file/324976)关键是限制副作用,清楚地识别它们,并避免在整个代码中分散它们。
错了,函数式编程会限制副作用,使程序易于理解和优化。甚至Haskell都允许您写入文件。
从本质上讲,我在说什么是功能程序员,认为副作用不是有害的,他们只是认为限制使用副作用是件好事。我知道这看起来像是一个简单的区分,但一切都与众不同。
readFile
的功能是定义一系列动作。该序列在功能上是纯净的,有点像描述要做什么的抽象树。然后由运行时执行实际的脏副作用。
一些注意事项:
没有副作用的功能可以并行执行,而有副作用的功能通常需要某种同步。
没有副作用的函数可以进行更积极的优化(例如,透明地使用结果缓存),因为只要我们获得正确的结果,函数是否真正执行甚至都没有关系
deterministic
为没有副作用的函数提供了一个子句,因此它们执行的次数不会超过必要的次数。
deterministic
子句只是一个关键字,它告诉编译器这是确定性函数,与final
Java中的关键字告诉编译器变量不能更改的方式类似。
我现在主要从事功能代码的研究,从这个角度来看,它似乎是显而易见的。副作用给试图阅读和理解代码的程序员带来了巨大的精神负担。在一段时间内没有负担之前,您不会注意到该负担,然后突然不得不再次读取具有副作用的代码。
考虑以下简单示例:
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我甚至不须看在之间的代码,更无法理解,或者看看它调用函数的实现。
关于并发,并行化和优化的所有内容都很不错,但这就是计算机科学家在手册中所写的内容。不必怀疑是谁在变异您的变量,什么时候是我在日常练习中真正享受的。
几乎没有语言会导致副作用。完全无副作用的语言将很难使用(几乎不可能),除非功能非常有限。
为什么副作用被认为是邪恶的?
因为它们使人们很难对程序的确切功能进行推理,并且很难证明程序可以执行您期望的操作。
在一个很高的层次上,想象一下仅用黑盒测试就可以测试整个3层网站。当然,这是可行的,具体取决于规模。但是肯定有很多重复。如果有是一个bug(即相关的副作用),那么你就可能打破整个系统进行进一步的测试,直到错误的诊断和修复,以及修复被部署到测试环境。
好处
现在,将其缩小。如果您相当擅长编写带有副作用的免费代码,那么您在推理某些现有代码的作用时会快多少?您编写单元测试的速度可以快多少?您如何确信没有副作用的代码可以保证没有错误,并且用户可以限制他们接触确实存在的任何错误?
如果代码没有副作用,则编译器可能还会执行其他优化。实施这些优化可能要容易得多。甚至可以概念化无副作用代码的优化,这可能要容易得多,这意味着您的编译器供应商可能会实现在具有副作用的代码中难以实现的优化。
当代码没有副作用时,并发也非常容易实现,自动生成和优化。这是因为所有部分都可以按任何顺序安全地评估。允许程序员编写高度并发的代码被广泛认为是计算机科学需要解决的下一个重大挑战,也是针对摩尔定律的少数对冲措施之一。
副作用就像代码中的“泄漏”,以后需要由您或一些毫无戒心的同事来处理。
功能语言避免使用状态变量和可变数据,这是使代码减少上下文依赖和模块化程度的一种方式。模块化确保一个开发人员的工作不会影响/破坏另一个开发人员的工作。
随团队规模扩展开发速度,是当今软件开发的“圣杯”。与其他程序员一起工作时,没有什么比模块性重要。即使是最简单的逻辑副作用也使协作变得极为困难。
好吧,恕我直言,这是很虚伪的。没有人喜欢副作用,但每个人都需要它们。
副作用的危险在于,如果您调用一个函数,那么这不仅会影响该函数在下次调用时的行为方式,而且可能还会影响其他函数。因此副作用带来了不可预测的行为和非平凡的依赖性。
诸如OO和函数之类的编程范例都解决了这个问题。OO通过强加关注点来减少问题。这意味着将包含许多可变数据的应用程序状态封装到对象中,每个对象仅负责维护自己的状态。这样,减少了依赖的风险,问题更加隔离,更易于跟踪。
函数式编程采用了一种更为激进的方法,即从程序员的角度来看,应用程序状态是完全不变的。这是一个不错的主意,但是使语言本身无用。为什么?因为任何I / O操作都有副作用。从任何输入流中读取数据后,应用程序状态可能会发生变化,因为下次调用相同的函数时,结果可能会有所不同。您可能正在读取其他数据,或者(也有可能)操作可能失败。输出也是如此。均匀输出是有副作用的操作。如今,您通常没有意识到这一点,但可以想象您只有20K的输出,如果再输出,由于磁盘空间不足或其他原因,应用程序将崩溃。
所以是的,从程序员的角度来看,副作用是令人讨厌和危险的。大多数错误来自应用程序状态的某些部分以几乎模糊的方式互锁的方式,这种方式是通过未考虑到的,并且常常是不必要的副作用而造成的。从用户的角度来看,副作用是使用计算机的重点。他们不在乎内部发生什么或如何组织。他们做了一些事情,并期望计算机会相应地进行更改。
任何副作用都会引入额外的输入/输出参数,在测试时必须考虑这些参数。
这使代码验证变得更加复杂,因为环境不仅限于验证的代码,还必须引入周围的部分或全部环境(更新的全局代码位于该代码所在的环境中,而该全局代码又取决于该环境)。代码,而这又取决于驻留在完整的Java EE服务器中。
通过尝试避免副作用,可以限制运行代码所需的外部性。
以我的经验,良好的面向对象编程设计要求使用具有副作用的功能。
例如,以一个基本的UI桌面应用程序为例。我可能正在运行的程序在其堆上具有一个对象图,该对象图表示我的程序域模型的当前状态。消息到达该图中的对象(例如,通过从UI层控制器调用的方法调用)。响应于消息,修改堆上的对象图(域模型)。通知模型观察者任何更改,修改UI以及其他资源。
这些修改堆和修改屏幕的副作用的正确排列绝不是邪恶,而是OO设计的核心(在本例中为MVC模式)。
当然,这并不意味着您的方法应具有矫揉造作的副作用。而且,无副作用功能确实可以提高代码的可读性,有时甚至可以提高性能。
正如上面的问题所指出的那样,函数式语言并没有因此阻止代码产生副作用,而是为我们提供了管理在给定代码段中以及何时发生的副作用的工具。
事实证明,这将产生非常有趣的结果。首先,也是最明显的是,您已经可以使用副作用免费代码进行很多操作。但是,即使使用确实有副作用的代码,我们也可以做其他事情:
在复杂的代码库中,副作用的复杂交互是我发现最困难的事情。鉴于我的大脑运作方式,我只能亲自讲话。副作用和持续状态以及变异的输入等等使我不得不思考事情的“何时”和“何处”是正确性的原因,而不仅仅是每个功能中都发生了“什么”。
我不能只专注于“什么”。在彻底测试了一个会引起副作用的函数之后,我无法得出结论,因为它将在使用该代码的整个代码中散布可靠性,因为调用者可能仍会通过在错误的时间,错误的线程,错误的位置调用它来滥用它。订购。同时,没有副作用,仅在给定输入(不触摸输入)的情况下返回新输出的函数,这种方式几乎是不可能被滥用的。
但是,我认为,或者至少是尝试,我是一个务实的人,而且我认为我们不必为了使我们的代码正确(至少我会发现使用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)。但是最重要的是,我发现第二个版本在正确性和更改方面都更容易做出推理,而不会引起错误。以便'
毕竟,我们需要在某个时候发生副作用,否则我们将只拥有无处可去的输出数据功能。通常,我们需要将某些东西记录到文件中,在屏幕上显示一些东西,通过套接字将数据发送出去,这种东西,而所有这些都是副作用。但是,我们绝对可以减少发生的多余副作用,并且还可以减少当控制流非常复杂时发生的副作用的数量,而且我认为如果这样做的话,避免错误会容易得多。
这不是邪恶的。我认为,有必要区分两种功能类型-有副作用和没有副作用。没有副作用的函数:-带有相同参数的返回总是相同的,因此例如没有任何参数的函数是没有意义的。-这也意味着,某些这样的函数所调用的顺序不起作用-必须能够运行并且可以单独调试(!),而无需任何其他代码。现在,大声笑,看看JUnit是什么。具有副作用的功能:-具有“泄漏”,可以自动突出显示-通过调试和查找错误(通常是由副作用引起的错误)非常重要。-任何具有副作用的功能本身也具有“部分”,而没有副作用,这些功能也可以自动分离。那些副作用是如此邪恶,