8

我刚拿到我的Expert F# 2.0副本,看到了这个声明,这让我有些惊讶:

例如,必要时,您可以对算法开始时分配的私有数据结构使用副作用,然后在返回结果之前丢弃这些数据结构;然后,总体结果实际上是一个无副作用的函数。与 F# 库分离的一个例子是库的 List.map 实现,它在内部使用了变异;写入发生在其他代码无法访问的内部分离数据结构上。

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

换句话说,如果抛开性能,List.map以纯粹的方式实现会更好吗?

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

4

7 回答 7

14

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

(我能想到的唯一弊端是,当一个人看到这样的局部可变对象时,他们必须推理它是否可以逃脱。在 F# 中,局部可变对象永远无法逃脱(闭包无法捕获可变对象),所以唯一的潜力“心理税”来自对可变引用类型的推理。)

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

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

于 2010-09-13T05:50:36.497 回答
6

您可能对 Simon Peyton Jones 的“Lazy Functional State Threads”感兴趣。我只看过前几页,非常清楚(我相信其余的也很清楚)。

重要的一点是,当你Control.Monad.ST习惯在 Haskell 中做这种事情时,类型系统本身会强制执行封装。在 Scala(可能还有 F#)中,这种方法更多的是“相信我们,我们不会ListBuffer在你的这个地方做任何鬼鬼祟祟的事情map”。

于 2010-09-13T04:47:49.150 回答
4

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

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

我应该补充一点,F# 中的一种常用技术是从一个纯函数式例程开始,然后在分析显示它太慢时通过利用可变状态(例如缓存、显式循环)对其进行优化。

于 2010-09-13T04:48:32.370 回答
3

Same approach can be found in use in Clojure. The immutable data structures in Clojure - list, map and vector - have their "transient" counterparts which are mutable. The Clojure reference about transient urges to use them only in the code which cannot be seen by "any other code".

There are guards against leaking transients in client code:

  • The usual function which work on the immutable data structures don't work on transients. Calling them will throw an exception.

  • The transients are bound to the thread they are created in. Modifying them from any other thread will throw an exception.

The clojure.core code itself uses a lot of transients behind the scenes.

The main benefit of using transients is the massive speed-up they provide.

So the tightly controlled use of mutable state seems to be OK in the functional languages.

于 2010-09-13T05:24:27.737 回答
2

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

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

于 2010-09-13T04:27:16.150 回答
2

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

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

于 2010-09-13T05:47:49.920 回答
0

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

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

作为用户,根本不关心函数的内部结构。从现在到审判日,它可以用字节码编码并在内部使用硬副作用,只要它符合人们期望的数据输入->数据输出合同。一个函数是一个黑匣子或一个预言机,它的内部结构是无关紧要的(假设它没有做任何愚蠢和外部的事情)。

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

许多人开发了一个功能然后使用,因此这两个方面都适用。

不变性与可变结构的优势是什么是另一个问题。

于 2010-09-13T19:40:16.133 回答