我觉得这种副作用是一种自然现象。但这就像功能性语言中的禁忌一样。什么原因?

我的问题特定于功能编程样式。并非所有编程语言/范式。

有帮助吗?

解决方案

编写无副作用的功能/方法 - 因此 纯函数 - 使推理程序的正确性变得更加容易。

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

它还可以使某些优化成为可能,例如编译器可以在其中备注函数结果或使用常见的子表达消除。

编辑:根据Benjol的要求:因为您所在州存储在堆栈中(数据流而不是控制流,如Jonas所说的 这里),您可以并行化或以其他方式重新排序相互独立的计算部分的执行。您可以轻松找到那些独立的零件,因为一个零件不能为另一部分提供输入。

在带有调试器的环境中,您可以回滚堆栈和恢复计算(例如SmallTalk),具有纯函数意味着您可以很容易地看到值的变化,因为以前的状态可用于检查。在突变重的计算中,除非您明确地将DO/UNDO动作添加到结构或算法中,否则您将看不到计算的历史记录。 (这与第一段联系在一起:编写纯函数使得更容易 检查 您程序的正确性。)

其他提示

从关于 功能编程:

实际上,应用需要产生一些副作用。功能性编程语言Haskell的主要贡献者Simon Peyton-Jones说:“最终,任何程序都必须操纵状态。一个没有任何副作用的程序是一种黑匣子。盒子变热了。” ((http://oscon.blip.tv/file/324976)关键是要限制副作用,清楚地识别它们,并避免将它们散射到整个代码中。

您已经做错了,功能性编程会促进限制副作用,以使程序易于理解和优化。甚至Haskell都允许您写入文件。

从本质上讲,我的意思是,功能性程序员不认为副作用是邪恶的,他们只是认为限制使用副作用是好的。我知道这似乎是一个简单的区别,但这一切都会有所不同。

一些注释:

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

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

我现在主要从事功能代码工作,从这个角度来看,它似乎很明显。副作用创建一个 巨大的 试图阅读和理解代码的程序员的心理负担。您不会注意到这一负担,直到您有一段时间释放它,然后突然不得不再次读取具有副作用的代码。

考虑这个简单的示例:

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层网站。当然,这是可行的,具体取决于规模。但是肯定有很多重复。如果那里 一个错误(与副作用相关),然后您可能会破坏整个系统以进行进一步测试,直到诊断和修复错误,并将修复程序部署到测试环境中。

好处

现在,将其缩小。如果您相当擅长编写副作用的免费代码,那么您在某些现有代码上的推理速度会更快?您可以更快地编写单元测试?您将如何自信地保证没有副作用的代码是没有错误的,并且用户可以将其限制在任何错误上 做过 有?

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

并发也非常简单地实现,自动生成并在没有副作用的情况下进行优化。这是因为所有零件都可以按任何顺序安全评估。允许程序员能够编写高度并发代码被广泛认为是计算机科学需要解决的下一个重大挑战,而剩下的少数套期保值之一是 摩尔定律.

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

功能语言避免了状态变量和可变数据,作为使代码较小的上下文依赖性和更模块化的一种方式。模块化确保一个开发人员的工作不会影响/破坏另一个开发人员的工作。

按团队规模的规模开发率是当今软件开发的“圣杯”。与其他程序员合作时,很少有东西像模块化一样重要。即使是最简单的逻辑副作用,协作也极为困难。

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

副作用是如此危险的是,如果您调用功能,那么这可能不仅对函数下次调用时的行为方式产生效果,而且可能对其他功能产生这种影响。因此,副作用引入了不可预测的行为和非平凡的依赖性。

编程范例(例如OO)和功能性都解决了这个问题。 OO通过施加关注点来减少问题。这意味着由许多可变数据组成的应用状态被封装在对象中,每个对象仅负责维护自己的状态。这样,依赖性风险就会降低,问题更加孤立,更易于跟踪。

功能编程采用了一种更加激进的方法,从程序员的角度来看,应用状态完全不可变。这是一个很好的主意,但使该语言本身无用。为什么?因为任何I/O-操作都有副作用。一旦您从任何输入流阅读,您的应用程序状态就可能会发生变化,因为下次调用相同的功能时,结果可能会有所不同。您可能正在阅读不同的数据,或者 - 也可能是一种可能性 - 操作可能会失败。输出也是如此。甚至输出也是具有副作用的操作。如今,您经常意识到这一点,但是想象您只有20K可以容纳20k,如果您的输出更多,则应用程序会崩溃,因为您不在磁盘空间之外。

因此,是的,从程序员的角度来看,副作用是讨厌和危险的。大多数错误来自应用程序状态的某些部分以几乎晦涩难懂的方式通过未经考虑和经常不必要的副作用而互锁的方式。从用户的角度来看,副作用是使用计算机的重点。他们不在乎内部发生的事情或组织方式。他们做点什么,并期望计算机会发生相应的变化。

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

这使得代码验证更加复杂,因为环境不能仅限于被验证的代码,但必须引入某些或全部周围的环境(该代码中更新的全局生命的全局,这又取决于该环境代码又取决于生活在完整的Java EE服务器中。

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

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

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

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

当然,这并不意味着您的方法应该具有仲裁副作用。和副作用的自由功能确实在提高代码的读取性,有时甚至有时性能方面都有席位。

邪恶有点超过顶部。这一切都取决于语言使用的上下文。

对已经提到的人的另一个考虑因素是,如果没有功能副作用,它可以简单地证明程序的正确性。

正如上面的问题所指出的那样,功能性语言不会如此 防止 代码从具有副作用作为为我们提供工具来管理给定代码和何时会发生什么副作用的工具。

事实证明,这会带来非常有趣的后果。首先,最明显的是,您可以使用副作用的免费代码来处理许多事情,这些代码已经描述了。但是,即使处理确实具有副作用的代码,我们也可以做其他事情:

  • 在具有可变状态的代码中,我们可以以静态确保不能在给定功能之外泄漏的方式管理状态的范围,这使我们能够在不参考计数或标记和扫描样式方案的情况下收集垃圾,但仍然确保没有参考可以生存。相同的保证也可用于维护对隐私敏感的信息等(可以使用Haskell中的ST Monad实现)
  • 在多个线程中修改共享状态时,我们可以通过跟踪更改并在交易结束时执行原子更新,或者将事务返回并重复此操作,如果另一个线程进行了冲突的修改,则可以避免使用锁。这仅是可以实现的,因为我们可以确保该代码除了国家修改(我们可以快乐地放弃)外没有其他效果。这是由Haskell中的STM(软件交易记忆)单元执行的。
  • 我们可以跟踪代码的效果和琐碎的砂盒,过滤它可能需要执行的任何效果,以确保其安全,从而允许(例如) 用户输入的代码将在网站上安全执行

在复杂的代码库中,副作用的复杂相互作用是我发现的最困难的事情。考虑到我的大脑的工作方式,我只能个人发言。副作用和持久状态并突变投入等,使我必须考虑“何时”和“在哪里”事物发生在正确性上,而不仅仅是每个单个函数中正在发生的事情。

我不能只专注于“什么”。在彻底测试了导致副作用的函数之后,我无法得出结论,它将在整个代码中使用它传播可靠性的气氛,因为呼叫者仍可能通过在错误的时间,错误的线程,错误的情况下滥用它来滥用它。命令。同时,一个没有引起副作用的函数,只需返回给定输入的新输出(不触摸输入),几乎不可能以这种方式滥用。

但是我认为我是一种务实的类型,或者至少尝试成为,而且我认为我们不必必须将所有副作用均为最低限度,以推理我们的代码的正确性(至少我会发现这样的语言很难做到这一点。在我发现正确性很难推理的地方是,当我们结合了复杂的控制流和副作用时。

复杂的控制流向我的是像图形的图形一样,通常是递归或递归状的(例如,事件队列,例如,它们不是直接递归地调用事件,而是“递归状的”,也许是在做事在遍历实际链接的图形结构或处理非均匀事件队列的过程中,包含事件的折衷混合物,以处理导致我们进入代码库的各种不同部分,并触发不同的副作用。如果您试图绘制所有您最终最终进入代码的地方,它将类似于一个复杂的图形,并且可能与您从未想到的图中的节点在那一刻,并且鉴于它们都是引起副作用,这意味着您可能不仅会对所谓的功能感到惊讶,而且还会在此期间发生什么副作用以及它们发生的顺序。

功能性语言可以具有极其复杂和递归的控制流,但是在正确性方面,结果很容易理解,因为在此过程中并没有进行各种折衷的副作用。只有当复杂的控制流量符合折衷的副作用时,我才发现它引起了头痛,以尝试理解正在发生的一切以及它是否总是做正确的事情。

因此,当我有这些案例时,我经常发现对这种代码的正确性感到非常自信,更不用说我可以非常自信地对这种代码进行更改而不绊倒意外的事情。因此,对我的解决方案要么简化控制流或最小化/统一副作用(通过统一,我的意思是,就像在系统中的特定阶段一样,对许多事物造成一种类型的副作用,而不是两个或三个或三个或打)。我需要这两件事之一,以使我的简单大脑对存在的代码的正确性以及我介绍的更改的正确性感到自信。如果副作用均匀且简单以及控制流程,那么对代码的正确性充满信心,这很容易被信任:

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

...这种效果是简化的一种迭代。这意味着我们已经多次通过数据,这绝对会产生计算成本,但是我们经常发现我们可以更轻松地更轻松地多线程,因为既然副作用和控制流都对这种统一和更简单的性质采取了措施。此外,每个循环都可以使缓存更友好地友好地友好,而遍历连接的图并引起副作用(例如:使用平行位设置来标记需要遍历的内容,以便我们可以按顺序顺序进行延迟通过使用bitmasks和ffs)。但最重要的是,我发现第二个版本在正确性和更改方面更容易推理,而不会引起错误。因此,无论如何,这就是我的处理方式,我应用了相同的思维方式来简化上面的网格处理,以简化事件处理等 - 更均匀的循环,带有死去的简单控制流,从而导致统一的副作用。

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

这不是邪恶的。我的看法,有必要区分两种功能类型 - 具有副作用,没有副作用。没有副作用的功能: - 返回与相同的参数始终相同,因此,例如没有任何参数的此功能没有任何意义。 - 这也意味着,某些功能所谓的命令无角色 - 必须能够运行,并且可以单独调试(!),而没有任何其他代码。现在,大声笑,看看junit的生产。具有副作用的功能: - 具有“泄漏”,可以自动突出显示的功能 - 通过调试和搜索错误非常重要,这通常是由副作用引起的。 - 任何具有副作用的功能也具有自身的“部分”,而没有副作用,也可以自动分离。这些副作用是如此邪恶,什么会导致难以追踪的错误。

许可以下: CC-BY-SA归因
scroll top