是否有人知道,当编程完全是功能性的而不是强制性的(即允许副作用)时,可能发生的最糟糕的渐进式减速是什么?
伊托尔森评论中的澄清:是否存在任何问题,其中最著名的非破坏性算法渐进地比最著名的破坏性算法更差,如果是,则差多少?
- 与强制编程时相同,无论是什么。
- @jldupont:返回计算结果。存在许多无副作用的程序。他们只能对输入进行计算。但这仍然有用。
- 我可以通过糟糕地编写我的函数代码使它变得像你想的那样糟糕!*我想你要问的是,"有没有什么问题,对于这个问题,最著名的非破坏性算法渐进地比最著名的破坏性算法差,如果是这样的话,有多少?"…对吗?
- 你能举一个你感兴趣的减速类型的例子吗?你的问题有点含糊。
- 什么?为什么这个问题被解决了?这是一个真实的问题,我提供了一个真实的答案(至少是部分答案,通过参考适当的文献)。为什么这个问题很难让其他人理解?对这个问题的显而易见的解释是伊托尔森提供的,我在下面已经回答了这个问题。我觉得奇怪的是,如此多的人跳到其他解释的问题,并投票结束它。
- @伊沃森-是的,这就是我的意思。我同意这句话措辞很差。
- 一位用户删除了他的答案,但他声称8皇后区问题的功能版本在n=13的情况下运行了超过一分钟。他承认这篇文章"写得不太好",所以我决定用f:pastebin.com/ffa8d4c4写自己的8个皇后版本。不用说,我的纯函数程序在一秒钟内计算出n=20。
- 关于reddit的一些好意见:reddit.com/r/programming/comments/c76b8/…
- 这个问题似乎没有引起人们的注意,因为它是关于计算机科学的。
根据Pippenger[1996]的观点,当比较一个纯粹的功能性(并且具有严格的评估语义,而不是懒惰的)Lisp系统和一个可以改变数据的系统时,为在O(n)中运行的不纯Lisp编写的算法可以翻译为在O(n logn)时间内运行的纯Lisp中的算法(基于Ben Amram和Galli[1992]AB的工作)。只使用指针模拟随机存取存储器)。Pippenger还确定,有一些算法是你能做的最好的;在不纯系统中有一些问题是O(n),在纯系统中是Ω(n logn)。好的。
关于这篇论文,有几点需要注意。最重要的是,它不处理懒惰的函数语言,如haskell。Bird、Jones和De Moor[1997]证明了Pippenger构造的问题可以在O(n)时间内用一种懒惰的函数语言来解决,但它们不能确定(据我所知,没有人能够确定)一种懒惰的函数语言是否能在与一种突变语言相同的渐进运行时间内解决所有问题。好的。
Pippenger提出的问题需要专门构造Ω(n logn)来实现这一结果,并不一定代表实际的现实问题。对这个问题有一些意想不到的限制,但这对证明工作是必要的;特别是,这个问题要求在线计算结果,而不能够访问未来的输入,并且输入由来自一组无限可能原子的原子序列组成,而不是由一组固定大小的原子组成。本文只建立了线性运行时间不纯算法的(下界)结果,对于需要较大运行时间的问题,在较大运行时间的算法所需的额外操作过程中,线性问题中的额外O(log n)因子可能被"吸收"。Ben Amram[1996]简要探讨了这些澄清和开放性问题。好的。
在实践中,许多算法可以在纯函数语言中实现,其效率与具有可变数据结构的语言相同。关于高效实现纯功能数据结构的技术,请参阅Chris Okasaki的"纯功能数据结构"【Okasaki 1998年】(这是他的论文的扩展版本【Okasaki 1996年】)。好的。
任何需要在纯功能数据结构上实现算法的人都应该阅读《冈崎》。通过使用平衡二叉树模拟可变内存,每次操作都会遇到最坏的O(log n)减速,但在许多情况下,您可以做得比这更好,并且冈崎介绍了许多有用的技术,从摊余技术到以增量方式进行摊余工作的实时技术。纯功能数据结构可能有点难以使用和分析,但它们提供了许多好处,例如在编译器优化、并行和分布式计算以及版本控制、撤消和回滚等功能的实现中有帮助的引用透明性。好的。
还要注意,所有这些只讨论渐进运行时间。许多实现纯功能数据结构的技术会给您带来一定量的常量因子减速,这是因为它们需要额外的簿记,以及所讨论语言的实现细节。纯功能数据结构的好处可能超过这些常量因子的减速,因此您通常需要根据所讨论的问题进行权衡。好的。工具书类
- 本阿姆兰,阿米尔和加利利,ZVI 1992年。"关于指针与地址的对比"ACM杂志,39(3),第617-648页,1992年7月
- Ben Amram,Amir,1996年。"关于皮蓬对纯粹与不纯口齿不清的比较的注释",未发表的手稿,丹麦哥本哈根大学迪库
- Bird、Richard、Jones、Geraint和De Moor,Oege,1997年。"更快、更快:懒惰与渴望的评估",《功能编程杂志》第7期,第5页,541-547页,1997年9月
- Okasaki,Chris,1996年。"卡内基梅隆大学纯粹功能数据结构博士论文
- 1998年,克里斯·冈崎。纯功能数据结构",剑桥大学出版社,英国剑桥
- 皮蓬,尼古拉斯,1996年。纯Lisp与不纯Lisp"ACM编程语言原理研讨会",第104-109页,1996年1月
好啊。
- 皮平格是这个问题上无可争议的权威。但是我们应该强调他的结果是理论上的,而不是实际的。当涉及到使功能数据结构实用和高效时,你不能比冈崎做得更好。
- 哇,谢谢,布莱恩,知道这个很有趣!既然我很难理解皮蓬格的解释,我可以问你:这些"不能做得更好"的算法是人为构建的病理案例,以显示纯粹系统和不纯粹系统之间存在无法弥合的差异,还是它们代表了"在野外"经常遇到的算法?谢谢!
- 伊托尔森:我必须承认,我没有读到足够多的皮蓬格来回答你的问题;它发表在一本同行评议的期刊上,被冈崎引用,我读了足够多的书来确定他的主张与这个问题有关,但不足以理解证据。对于现实世界的后果,我得到的直接结论是,通过简单地使用平衡二叉树模拟可修改的内存,将O(N)不纯算法转换为O(N对数N)纯算法是很简单的。有一些问题不能比这更好;我不知道它们是否纯粹是理论上的。
- Pippenger结果做出了两个限制其范围的重要假设:它考虑"在线"或"反应"计算(不是将有限输入映射到单个输出的计算的通常模型)和"符号"计算,其中输入是原子序列,只能对其进行等同性测试(即,输入的解释是非常原始)。
- 很好的答案;我想补充一点,对于纯函数语言来说,计算复杂度没有一个普遍认同的模型,而在不纯的世界里,单位成本的RAM机器是相对标准的(这使得比较起来更加困难)。还请注意,纯/不纯的lg(n)差的上界可以通过查看纯语言中数组的实现很容易地直观地解释(每次操作(并且您得到历史记录)会花费lg(n))。
- 纯功能清理(clean.cs.ru.nl)语言允许通过唯一性类型进行破坏性更新。对于唯一性类型是否适用于所引用结果中内置的假设,有人能评论一下吗?
- 从来没有用clean编程过,但是我猜唯一性类型类似于haskell io monad,它允许以引用透明的方式编写命令式代码,因此它们不受这些界限的影响。例如,在haskell中,您可以写入:var<-readln:var2<-readln,由于一个不同的(隐式)状态参数,对readln的两个调用给出了不同的结果。在clean中,你可以做类似的事情,你可以有一个命令式数组库,比如haskell。关于"参照透明",参见西蒙·佩顿·琼斯(SimonPeytonJones)的《对付尴尬的队伍》(Haskell)。
- 重要的是要注意,虽然许多事情可以以纯粹的功能性方式高效地实现,但一些重要的事情却不能。例如,纯函数字典比可变字典(特别是哈希表)慢几倍。纯函数数组比可变数组慢得多。
- @朱尔斯:"纯函数数组"是什么意思?使用纯函数数组显然是一个坏主意——问题是是否存在性能足够好的纯函数替换。也许你就是这么说的,但我不确定。哈希图和数组(共享同一个问题)都可以用更多的树型结构(非正式地说是数组树)来替换;例如,scala的纯函数数组(vector)版本比可变数组慢(盯着代码一点也不难相信),但我不知道有多慢。
- @朱尔斯,这不是我在回答结束时说的吗?"许多实现纯功能数据结构的技术都会给您带来一定量的常量因子减速,这是因为它们需要额外的簿记,以及所讨论语言的实现细节。"整个答案是,在某些情况下,渐进减速是不可避免的。我不知道你想补充什么。
- @布莱恩,你的答案清楚地证明了减速最多是对数的,但是很难知道这个理论事实是如何影响实践的。朱尔斯补充说,在许多现实场景中,简单地用持久结构替换可变数组会破坏程序的性能。对数成本,以及更高的常数因子,在实践中真的很重要。我自己也曾认为对数减速会被忽略或负担得起,即使在注重性能的情况下,也常常发现相反。
- (不过,我认为散列表的问题不太清楚,特别是因为Clojure人对散列数组映射的尝试进行了研究。)
- 我认为真正的问题是编译器是否可能确定不纯和纯方法是等价的,并将后者转换为前者。当然,往另一个方向走会更困难。
- 重要的一点:如果您最终(自动或手工)将纯功能规范翻译成更复杂、更高效的纯功能实现,那么将其翻译成更高效、更不纯净的代码将没有什么好处。如果你能把它关在笼子里,比如把它锁在一个没有外部副作用的功能里,那么杂质就不算什么了。
- @Robin所有函数语言的编译器最终都会生成不纯的代码。(纯)函数语言的目标之一是使程序员避免处理难以推理和难以并行的概念,如状态和变异。当然,您可以做一些功能代码。但这并不能真正避免必须处理突变和状态的问题,即使您有一个封装非功能代码的"功能接口"。当然,编写命令式代码有一些好的、有些可组合的方法,但是纯粹性仍然有好处。
确实有几种算法和数据结构,对于这些算法和数据结构,即使存在惰性,也不知道渐进有效的纯函数解(t.i.在纯lambda微积分中可实现的一个)。
然而,我们假设在"命令式"语言中,对内存的访问是O(1),而在理论上,不可能是渐进的(即对于无边界的问题大小),在一个巨大的数据集中对内存的访问总是O(log n),可以用函数式语言来模拟。
此外,我们必须记住,实际上所有现代函数语言都提供可变的数据,Haskell甚至在不牺牲纯度的情况下提供这些数据(St Monad)。
- 如果数据集适合物理内存,那么对它的访问是O(1),因为可以找到读取任何项所需时间的绝对上限。如果数据集没有,那么您所说的是I/O,而这将是到目前为止的主导因素,不管程序是如何编写的。
- 当然,我是在讨论访问外部内存的O(log n)操作。然而,在任何情况下,我说的是B:外部存储器也可以是O(1)-可寻址的…
- 我认为,与函数式编程相比,命令式编程所获得的最大好处之一是能够保持对一个状态的许多不同方面的引用,并生成一个新状态,以便所有这些引用都指向新状态的相应方面。使用函数式编程需要用查找操作替换直接取消引用操作,以找到当前总体状态的特定版本的适当方面。
- 即使是指针模型(O(log n)内存访问,松散地说)在很大的规模上也不实际。光速限制了不同的计算设备彼此之间通信的速度,而目前人们认为,在给定区域内可以保存的最大信息量受其表面积的限制。
本文声称已知的union-find算法的纯函数实现都比它们发布的具有纯函数接口但在内部使用可变数据的算法具有更差的渐进复杂性。
其他答案声称永远不会有任何区别,例如,纯函数代码的唯一"缺点"是它可以并行化,这让您了解函数编程社区在这些问题上的信息性/客观性。
编辑:
下面的评论指出,纯函数编程的优缺点的有偏见的讨论可能不是来自"函数编程社区"。好点。也许我看到的拥护者只是引用了一句话,"文盲"。
例如,我认为这个博客文章是由一个可以说是功能编程社区代表的人写的,因为它是一个"懒惰评估的要点"列表,所以它是一个很好的地方来提到懒惰和纯功能编程可能存在的任何缺点。一个好的地方可以代替以下(技术上是正确的,但有点不好笑)解雇:
If strict a function has O(f(n)) complexity in a strict language then it has complexity O(f(n)) in a lazy language as well. Why worry? :)
对于内存使用的固定上限,应该没有区别。
证明草图:给定一个固定的内存使用上限,应该能够编写一个虚拟机,该虚拟机以相同的渐进复杂性执行命令集,就好像您实际上在该机器上执行一样。这是因为您可以将可变内存作为持久的数据结构来管理,给O(log(n))提供读和写,但是使用固定的内存使用上限,您可以拥有固定数量的内存,从而使这些内存衰减为O(1)。因此,功能实现可以是运行在虚拟机功能实现中的必要版本,因此它们都应该具有相同的渐进复杂性。
- 关于内存使用的固定上限不是人们如何分析这类事情;您假设一个任意大但有限的内存。在实现算法时,我对它如何从最简单的输入扩展到任意的输入大小感兴趣。如果对内存使用量设置一个固定的上限,为什么不同时对允许计算花费的时间设置一个固定的上限,并假设所有内容都是O(1)?
- @布莱恩坎贝尔:是的。我只是建议,如果你愿意的话,在实践中,你可以忽略常数因子的差异。当在内存和时间之间进行折衷时,仍然需要注意区别,以确保使用多m倍的内存将运行时减少至少一个对数(m)因子。
我建议阅读haskell的性能,然后看看函数语言和过程/oo语言的基准游戏性能。