纯函数式编程的效率


397

有谁知道纯功能编程而不是强制性编程(即允许副作用)发生时,最糟糕的渐近减慢可能是什么?

从itowlson的评论中澄清:是否存在最知名的无损算法比最知名的破坏性算法渐近恶化的问题?


6
无论是什么,都与命令式编程时相同。
R. Martinho Fernandes

3
@jldupont:返回当然的计算结果。存在许多无副作用的程序。除了计算输入之外,他们无能为力。但这仍然有用。
jalf

24
通过糟糕地编写我的功能代码,我可以使它变坏!*咧嘴*我想您要问的是“是否存在最著名的非破坏性算法比最著名的破坏性算法渐近恶化的问题? ?
itowlson'1

2
您能否举例说明您感兴趣的减速类型。您的问题有点含糊。
Peter Recore 2010年

5
一个用户删除了他的答案,但他声称8皇后问题的功能版本在n = 13的一分钟内就运行了。他承认这不是“写得很好”,所以我决定编写自己的版本。 F#中的8个皇后:pastebin.com/ffa8d4c4。不用说,我的纯函数程序仅需一秒钟即可计算n = 20。
朱丽叶

Answers:


531

根据Pippenger [1996]的研究,当将纯功能的Lisp系统(具有严格的评估语义,而不是惰性的)与可以变异数据的Lisp系统进行比较时,可以翻译为在O(n)中运行的不纯Lisp编写的算法。转换为纯Lisp中运行时间为O(n log n)的算法(基于Ben-Amram和Galil [1992]关于仅使用指针模拟随机存取存储器的工作)。皮蓬格(Pippenger)还确定,有些算法是您可以做的最好的事情。在不纯系统中存在O(n)在纯系统中存在Ω(n log n)的问题。

关于本文有一些警告。最重要的是,它不解决惰性函数语言,例如Haskell。Bird,Jones和De Moor [1997]证明由Pippenger构造的问题可以在O(n)时间内以一种惰性函数语言解决,但它们并没有确定(据我所知,没有人知道)是否没有一种懒惰的功能语言可以像变种语言一样在相同的渐近运行时间内解决所有问题。

由皮蓬格(Pippenger)构造的问题需要专门构造Ω(n log n)才能获得此结果,并且不一定代表实际的实际问题。对这个问题有一些限制,这是出乎意料的,但是对于证明工作是必不可少的。特别地,该问题要求结果是在线计算的,而无法访问将来的输入,并且该输入由来自无限可能原子集合的原子序列组成,而不是固定大小的集合。并且本文仅针对线性运行时间的不纯算法建立(下界)结果。对于需要更长运行时间的问题,可能会额外增加O(log n线性问题中出现的因素可能在具有更长运行时间的算法所需的额外操作过程中被“吸收”。Ben-Amram [1996]简要地探讨了这些澄清和开放性问题。

实际上,可以用纯功能语言以与具有可变数据结构的语言相同的效率来实现许多算法。有关有效用于实现纯功能数据结构的技术的良好参考,请参见Chris Okasaki的“纯功能数据结构” [Okasaki 1998](这是其论文的扩展版本[Okasaki 1996])。

任何需要在纯功能数据结构上实现算法的人都应该阅读Okasaki。通过用平衡的二叉树模拟可变内存,您每次操作总是最糟糕的情况是O(log n)变慢,但在许多情况下,您可以做得更好,而且Okasaki描述了许多有用的技术,从摊销技术到实物化,逐步进行摊销的时间。纯功能数据结构可能很难使用和分析,但是它们提供了很多好处,例如引用透明性,有助于编译器优化,并行和分布式计算以及实现诸如版本控制,撤消和回滚之类的功能。

还要注意,所有这些仅讨论渐近的运行时间。许多实现纯功能数据结构的技术会给您一定程度的恒定因子减慢,这是因为它们需要额外的簿记工作以及所涉及语言的实现细节。纯功能数据结构的好处可能胜过这些恒定因素的减速,因此您通常需要根据相关问题进行权衡。

参考文献


50
在这个问题上,皮蓬格(Pippinger)是无可争议的权威。但是我们应该强调,他的结果是理论上的,而不是实际的。在使功能性数据结构切实可行和高效方面,您无法比Okasaki做得更好。
诺曼·拉姆齐

6
itowlson:我必须承认,我对Pippenger的阅读不足,无法回答您的问题;它被发表在Okasaki引用的同行评审期刊上,我读了足够多的书以确定他的主张与该问题有关,但不足以理解该证据。我直接想到的现实结果是,通过使用平衡二叉树简单地模拟可修改的内存,将O(n)不纯算法转换为O(n log n)纯算法是微不足道的。有些问题不能比这更好。我不知道它们是否纯粹是理论上的。
布莱恩·坎贝尔

3
Pippenger结果有两个重要的假设限制了它的范围:它考虑“在线”或“反应性”计算(不是将有限输入映射到单个输出的计算的常规模型)和“符号”计算,其中输入是以下各项的序列只能测试相等性的原子(即,输入的解释是极其原始的)。
克里斯·康威

2
很好的答案;我想补充一点,对于纯功能语言,还没有公认的计算复杂性模型,而在不纯净的世界中,单位成本的RAM机器是相对标准的(因此这使得比较事情变得更加困难)。还要注意,通过查看纯语言的数组实现,可以很容易地直观地解释纯净/不纯Lg(N)差异的上限(每次操作花费lg(n)(您会得到历史记录)) 。
user51568 2010年

4
要点:如果最终要(自动或手动)将纯函数规范转换为更有效的不纯代码,那么将纯函数规范转换为更复杂的有效纯函数实现几乎没有什么好处。如果您可以将杂质保存在笼子中,例如通过将其锁定在无外部副作用的功能中,那么杂质并不是什么大问题。
罗宾·格林

44

确实有几种算法和数据结构,即使是惰性的,也没有已知的渐近有效的纯函数解(在纯lambda演算中可以实现)。

  • 前述工会发现
  • 哈希表
  • 数组
  • 一些图算法
  • ...

但是,我们假设在“命令式”语言中,对内存的访问为O(1),而在理论上不能如此渐近(即对于无限制的问题大小),并且对庞大数据集中的内存的访问始终为O(log n) ,可以使用功能语言进行仿真。

同样,我们必须记住,实际上所有现代功能语言都提供可变数据,Haskell甚至在不牺牲纯度的情况下提供了数据(ST monad)。


3
如果数据集适合物理内存,则对它的访问为O(1),因为可以找到读取任何项目的时间的绝对上限。如果数据集不存在,那么您正在谈论的是I / O,到目前为止,这是主要的因素,但是编写了程序。
Donal Fellows 2010年

好吧,我当然是在谈论访问外部存储器的O(log n)操作。但是,无论如何,我在说bs:外部存储器也可以是O(1)可寻址的...
jkff 2010年

2
我认为与函数式编程相比,命令式编程所获得的最大好处之一就是能够对一个状态的许多不同方面进行引用,并生成一个新状态,从而使所有这些引用都指向该新状态的相应方面。使用函数式编程将需要使用查找操作来替换直接解引用操作,以查找当前总体状态的特定版本的适当方面。
超级猫

甚至指针模型(从广义上来说,O(log n)内存访问)在物理上也不现实。光速限制了不同计算设备之间相互通信的速度,而目前人们认为,可以在给定区域中保存的最大信息量受其表面积限制。
dfeuer

36

本文声称,联合查找算法的已知纯功能实现都比它们发布的算法具有更差的渐进复杂度,后者具有纯功能接口,但内部使用可变数据。

其他答案声称永远没有任何区别,例如,纯函数代码的唯一“缺点”是它可以并行化,这一事实使您对函数式编程社区在这些问题上的了解程度/客观性有所了解。

编辑:

下面的评论指出,对纯函数式编程的优缺点的偏颇的讨论可能不会来自“函数式编程社区”。好点子。也许我看到的倡导者只是在评论中说是“文盲”。

例如,我认为此博客文章是由可以说是功能编程社区代表的人撰写的,并且由于它是“懒惰评估的要点”列表,因此它是提及任何缺点的好地方懒惰和纯函数式编程可能具有。解雇以下人员是一个不错的选择(从技术上讲是正确的,但偏向于不搞笑):

如果严格函数在严格语言中具有O(f(n))复杂度,那么在懒惰语言中函数也具有O(f(n))复杂度。为什么要担心?:)


4

在固定的内存使用上限上,应该没有区别。

证明草图:给定固定的内存使用上限,一个人应该能够编写一台虚拟机,该虚拟机以与您实际在该计算机上执行相同的渐进复杂度执行命令式指令集。之所以这样,是因为您可以将可变内存作为持久性数据结构进行管理,从而实现O(log(n))的读写,但是由于内存使用量的上限是固定的,因此您可以拥有固定的内存量,从而导致这些衰减到O(1)。因此,功能实现可以是在VM的功能实现中运行的命令式版本,因此它们都应具有相同的渐近复杂度。


6
内存使用量的固定上限不是人们如何分析这类事情。您假定任意大但有限的内存。在实现算法时,我对它将如何从最简单的输入扩展到任意输入大小感兴趣。如果对内存使用量设置了固定的上限,为什么不对允许计算花费多长时间也设置固定的上限,并说一切都是O(1)?
布莱恩·坎贝尔

@布莱恩·坎贝尔:是的。我只是建议,如果您愿意,在实践中许多情况下可以忽略常数因子的差异。仍然需要注意内存和时间之间的差异,以确保使用m倍的内存可以将运行时间至少减少log(m)倍。
布赖恩2010年

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.