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

J C*_*per 8 monads f# haskell functional-programming side-effects

我刚收到了专家F#2.0的副本并且发现了这个声明,这有点让我感到惊讶:

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

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

换句话说,如果放弃表现,那么List.map以纯粹的方式实施会更好吗?

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

Bri*_*ian 14

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

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

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

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

  • 优化编译器是一个神话(除了可能在Haskell中).好吧,这个陈述太强了,但是_impure_语言的编译器在"优化"纯代码方面做得比在人类用不纯净的代码手动优化纯色代码要好得多.编译器根本就不那么好. (3认同)

Tra*_*own 6

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

重要的是,当您Control.Monad.ST在Haskell中使用这种方法时,类型系统本身会强制执行封装.在斯卡拉(也许F#)的办法就是多"请相信我们,我们什么都没有做偷偷摸摸的在这里这个ListBuffer在你的map".


Gab*_*abe 5

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

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

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