纯函数式编程语言如何处理快速变化的数据?


22

您可以使用什么数据结构来删除和替换O(1)?还是在需要上述结构时如何避免出现这种情况?


2
对于那些对纯函数式编程语言不太熟悉的人,您是否愿意提供更多的背景知识,以便我们了解您的问题所在?
FrustratedWithFormsDesigner 2014年

4
@FrustratedWithFormsDesigner纯粹的函数式编程语言要求所有变量都是不可变的,而这又需要数据结构,这些数据结构在“修改”时会创建自己的新版本。
2014年

5
您是否知道Okasaki在纯功能数据结构方面的工作?

2
一种可能性是为可变数据定义一个monad(例如,参见haskell.org/ghc/docs/4.08/set/sec-marray.html)。这样,可变数据的处理方式与IO相似。
乔治

1
@CodesInChaos:但是,这种不可变的结构通常比简单数组具有更多的开销。其结果是,存在非常实用的差异。这就是为什么任何针对通用编程的纯函数式语言都应该有一种使用可变结构的方式,该方式可以与纯语义兼容的某种安全方式。STHaskell 的monad非常出色。
leftaboutabout

Answers:


32

有大量的数据结构利用惰性和其他技巧来实现分摊的固定时间,甚至(对于某些有限的情况,例如队列)还可以对许多问题进行固定时间更新。Chris Okasaki的博士学位论文 “ Purely Functional Data Structures”和同名的书是一个很好的例子(也许是第一个主要的例子),但是自那以后该领域取得了进步。这些数据结构通常不仅在界面上具有纯粹的功能,而且还可以用纯Haskell和类似语言实现,并且是完全持久的。

即使没有任何这些高级工具,简单的平衡二进制搜索树也可以提供对数时间更新,因此可以模拟可变存储器,而最坏情况下,对数速度会降低。

还有其他选项可能被视为作弊,但对于实现工作和实际性能非常有效。例如,线性类型或唯一性类型允许通过防止程序保留到先前的值(将被更改的内存)来就地更新,以作为概念上纯净语言的实现策略。这比持久性数据结构的通用性要小:例如,您不能通过存储状态的所有先前版本来轻松构建撤消日志。尽管AFAIK在主要功能语言中尚不可用,但它仍然是一个强大的工具。

将可变状态安全地引入功能设置的另一种选择是STHaskell中的monad。可以实现它而无需进行任何更改,并且不添加任何unsafe*功能,它的行为就像只是一个花哨的包装,隐式地传递了持久性数据结构(参见参考资料State)。但是由于某种类型的系统欺骗会强制执行求值顺序并防止转义,因此可以通过就地突变安全地实现它,并具有所有性能优势。


也可能是值得一提的拉链让你在一个列表或树重点做快速变化的能力
JK。

1
@jk。我链接到的理论计算机科学文章中提到了它们。而且,它们只是许多相关数据结构中的一个(很好,是一类),对它们的讨论都超出了范围并且很少使用。

很公平,没有遵循链接
jk。

9

一种廉价的可变结构是参数堆栈。

看一看典型的SICP风格阶乘计算:

(defn fac (n accum) 
    (if (= n 1) 
        accum 
        (fac (- n 1) (* accum n)))

(defn factorial (n) (fac n 1))

如您所见,to的第二个参数fac用作包含快速变化的乘积的可变累加器n * (n-1) * (n-2) * ...。但是,看不到可变变量,也没有办法无意间更改累加器,例如从另一个线程更改累加器。

当然,这是一个有限的例子。

您可以通过廉价地替换头节点(并且扩展为从头开始的任何部分)来获得不可变的链表:您只需将新头指向旧头所指向的下一个节点即可。这对许多列表处理算法(fold基于任何东西)都适用。

您可以从基于HAMT的关联数组中获得相当不错的性能。从逻辑上讲,您会收到一个新的关联数组,其中某些键值对已更改。该实现可以在旧对象和新创建的对象之间共享大多数公共数据。不过这不是O(1);通常,至少在最坏的情况下,您会得到对数的东西。另一方面,与可变树相比,不可变树通常不会遭受任何性能损失。当然,这需要一些内存开销,通常远不是让人望而却步。

另一种方法基于这样的想法,即如果一棵树掉在森林中而没人听见,它就不需要发出声音。也就是说,如果您可以证明一点突变状态永远不会离开某些本地范围,则可以安全地对其内部的数据进行突变。

Clojure的瞬态是不可变数据结构的可变“影子”,不会泄漏到本地范围之外。Clean使用Uniques实现类似的目的(如果我没记错的话)。Rust通过静态检查的唯一指针来帮助做类似的事情。


1
+1,也用于提及“清理”中的唯一类型。
Giorgio

@ 9000我想我听说Haskell有类似于Clojure瞬态的东西-如果我错了,有人可以纠正我。
paul 2014年

@paul:我对Haskell有非常粗略的了解,因此,如果您能提供我的信息(至少是google的关键字),我很乐意提供对答案的引用。
9000

1
@保罗我不太确定。但是Haskell确实有一种创建类似于ML的东西ref并将它们限制在一定范围内的方法。请参阅IORefSTRef。然后当然还有TVars和MVars相似,但是具有并发的语义(针对TVars的stm 和基于MVars的互斥体)
Daniel Gratzer 2014年

2

您要问的内容太宽泛了。O(1)从哪个位置拆卸和更换?序列的头?尾巴?一个任意的位置?使用的数据结构取决于这些细节。就是说,2-3棵手指树似乎是最灵活的持久数据结构之一:

我们提出了2-3个手指树,这是一种持久性序列的功能性表示,支持在摊销的恒定时间内访问末端,并且在较小块的大小上按时间对数进行级联和拆分。

(...)

此外,通过以通用形式定义拆分操作,我们获得了通用数据结构,该数据结构可以用作序列,优先级队列,搜索树,优先级搜索队列等。

通常,当更改任意位置时,持久性数据结构具有对数性能。这可能是问题,也可能不是问题,因为O(1)算法中的常数可能很高,并且对数减慢可能会“吸收”到整体速度较慢的算法中。

更重要的是,持久性数据结构使对程序的推理更加容易,并且这应该始终是您的默认操作模式。您应该尽可能支持持久性数据结构,并且在分析并确定持久性数据结构是性能瓶颈之后,才应使用可变数据结构。其他一切都是过早的优化。

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.