对调用者看起来纯粹但在内部使用突变的函数

时间:2010-09-13 03:53:07

标签: haskell f# functional-programming monads side-effects

我刚拿到了 Expert F#2.0 的副本并且发现了这个声明,这让我感到有些惊讶:

  

例如,必要时,您可以   对私人数据使用副作用   在开始时分配的结构   算法然后丢弃这些   返回之前的数据结构   结果;那么整体结果就是   实际上是一种无副作用的   功能。分离的一个例子   来自F#库的是图书馆   List.map的实现,它使用   内部突变;写入发生   在内部,分离的数据上   没有其他代码可以的结构   访问。

现在,显然这种方法的优势在于性能。我只是好奇是否有任何缺点 - 副作用带来的任何陷阱都适用于此吗?并行性是否会受到影响?

换句话说,如果抛开绩效,是否最好以纯粹的方式实施List.map

(显然这尤其涉及F#,但我也对一般哲学感到好奇)

7 个答案:

答案 0 :(得分:14)

我认为副作用的几乎所有缺点都与“与程序其他部分的交互”有关。副作用本身并不坏(正如@Gabe所说,即使是纯粹的功能程序也在不断地改变RAM),这是导致问题的效果(非本地交互)的共同后果(具有调试/性能/可理解性) /等等。)。因此对纯粹本地状态的影响(例如对不能逃避的局部变量)是好的。

(我能想到的唯一不利因素是,当一个人看到这样一个本地变异时,他们必须推断它是否可以逃脱。在F#中,局部变异无法逃脱(闭包不能捕获变体),所以只有潜在的“精神税”来自对可变参考类型的推理。)

总结:使用效果很好,只要简单地说服一个人自己,效果只发生在非逃避的当地人身上。 (在其他情况下也可以使用效果,但我忽略了其他情况,因为在这个问题 - 线程上我们是开明的函数式程序员试图在合理的情况下避开效果。:))

(如果你想深入了解,那些局部效果,比如F#的List.map的实现,不仅不会妨碍并行化,而且实际上是一个好处,从更多的角度来看 - 高效的实现分配较少,因此对GC的共享资源的压力较小。)

答案 1 :(得分:6)

您可能对Simon Peyton Jones的"Lazy Functional State Threads"感兴趣。我只是通过前几页完成的,非常清楚(我确信其余部分也非常清楚)。

重要的一点是,当您在Haskell中使用Control.Monad.ST来执行此类操作时,类型系统本身会强制执行封装。在Scala(可能还有F#)中,这种方法更“信任我们,我们在ListBuffer”中使用此map并没有偷偷摸摸地做任何事情。

答案 2 :(得分:4)

如果函数使用本地,私有(对函数)可变数据结构,则并行化不受影响。因此,如果map函数在内部创建一个列表大小的数组并迭代其填充数组的元素,您仍然可以在同一列表上同时运行map 100次而不用担心,因为每个map的实例将拥有自己的私有数组。由于您的代码在填充之前无法看到数组的内容,因此它实际上是纯粹的(请记住,在某种程度上,您的计算机必须实际改变RAM的状态)。

另一方面,如果函数使用全局可变数据结构,则可能会影响并行化。例如,假设您有一个Memoize函数。显然,它的全部意义在于维持一些全局状态(虽然“全局”在某种意义上说它不是函数调用的本地状态,但它仍然是“私有的”,因为它在函数外部是不可访问的)所以它不必使用相同的参数多次运行函数,但它仍然是纯的,因为相同的输入将始终产生相同的输出。如果您的缓存数据结构是线程安全的(如ConcurrentDictionary),那么您仍然可以与自身并行运行您的函数。如果没有,那么你可能会认为该函数不是纯粹的,因为它具有在并发运行时可观察到的副作用。

我应该补充说,在F#中开始使用纯函数例程是一种常见的技术,然后在分析显示它太慢时利用可变状态(例如缓存,显式循环)来优化它。

答案 3 :(得分:3)

在Clojure中可以找到相同的方法。 Clojure中的不可变数据结构 - 列表,映射和向量 - 具有可变的“瞬态”对应物。 Clojure reference about transient强烈要求仅在“任何其他代码”无法看到的代码中使用它们。

在客户端代码中存在防止泄漏瞬间的措施:

  • 对不可变数据结构起作用的常用函数不适用于瞬态。调用它们会引发异常。

  • 瞬态绑定到它们创建的线程。从任何其他线程修改它们都会引发异常。

clojure.core代码本身在幕后使用了许多瞬态。

使用瞬态的主要好处是它们提供了大量的加速。

因此,在函数式语言中,可变状态的严格控制使用似乎没问题。

答案 4 :(得分:2)

它不会影响该功能是否可以与其他功能并行运行。它会影响函数的内部结构是否可以并行 - 但对于大多数针对PC的小函数(例如地图)来说,这不太可能是一个问题。

我注意到,一些优秀的F#程序员(在网络和书籍中)似乎对使用循环的命令式技术非常放松。他们似乎更喜欢带有可变循环变量的简单循环,而不是复杂的递归函数。

答案 5 :(得分:2)

一个问题是,构建一个好的函数编译器可以很好地优化“功能”代码,但是如果你使用了一些可变的东西,编译器可能不会像其他情况那样优化。在最坏的情况下,这会导致算法效率低于不可变变量。

我能想到的另一个问题是懒惰 - 一个可变的数据结构通常不是懒惰的,因此一个可变的命令可能会强制对参数进行不必要的评估。

答案 6 :(得分:0)

我会回答这个问题:“你在编写函数,还是使用函数?”

功能,用户和开发人员有两个方面。

作为用户,根本不关心函数的内部结构。它可以用字节代码编码,并且从现在开始直到判断日内部使用硬副作用,只要它匹配数据的合同,并且数据输出就是人们期望的。一个函数是一个黑盒子或一个oracle,它的内部结构是无关紧要的(假设它没有做任何愚蠢和外在的事情)。

作为一个功能的开发者,内部结构很重要。不变性,常规正确性和避免副作用都有助于开发和维护功能,并将功能扩展到并行域。

许多人开发了他们随后使用的功能,因此这两个方面都适用。

不变性与可变结构的优点是一个不同的问题。