Haskell:列表,数组,向量,序列


230

我正在学习Haskell,并阅读了几篇有关Haskell列表和(插入您的语言)数组的性能差异的文章。

作为学习者,我显然只使用列表,甚至没有考虑性能差异。我最近开始调查,发现Haskell中提供了许多数据结构库。

有人可以解释列表,数组,向量,序列之间的区别,而无需深入了解数据结构的计算机科学理论吗?

另外,是否有一些常见的模式可以使用一种数据结构代替另一种数据结构?

我还缺少其他任何形式的数据结构并且可能有用吗?


1
看一下有关列表与数组的答案:stackoverflow.com/questions/8196667/haskell-arrays-vs-lists向量的性能与数组基本相同,但是API更大。
GrzegorzChrupała2012年

看到此处讨论的Data.Map也很高兴。这似乎是有用的数据结构,尤其是对于多维数据。
马丁·卡波迪奇

Answers:


339

列出摇滚

到目前为止,Haskell中用于顺序数据的最友好的数据结构是List

 data [a] = a:[a] | []

列表为您提供ϴ(1)缺点和模式匹配。标准库,并为此事的序幕,充满了有用的列表功能应该垃圾代码(foldrmapfilter)。列表是持久性的,也就是纯功能性的,这非常好。Haskell列表并不是真正的“列表”,因为它们是共性的(其他语言称为这些流),因此类似

ones :: [Integer]
ones = 1:ones

twos = map (+1) ones

tenTwos = take 10 twos

工作出色。无限的数据结构无处不在。

Haskell中的列表提供了一个界面,就像命令式语言中的迭代器一样(由于懒惰)。因此,它们被广泛使用是有道理的。

另一方面

列表的第一个问题是索引到列表(!!)需要ϴ(k)时间,这很烦人。此外,附加操作可能很慢++,但是Haskell的惰性评估模型意味着,即使这些操作完全发生,也可以将其视为完全摊销。

列表的第二个问题是它们的数据本地性差。当内存中的对象没有彼此相邻布置时,实际处理器会产生高常量。因此,在C ++ std::vector中,“ snoc”(末尾放入对象)的速度比我所知道的任何纯链表数据结构都要快,尽管这并不是一个持久化的数据结构,它没有Haskell的列表那么友好。

列表的第三个问题是它们的空间效率很差。一堆额外的指针会增加您的存储空间(增加一个常数)。

序列是有功能的

Data.Sequence在内部基于手指树(我知道,您不想知道这一点),这意味着它们具有一些不错的属性

  1. 纯粹的功能。 Data.Sequence是一个完全持久的数据结构。
  2. 该死的人可以快速进入树的起点和终点。ϴ(1)(摊销)以获取第一个或最后一个元素,或追加树。在事物清单上最快的是Data.Sequence,最多是恒定不变的。
  3. log(log n)访问序列的中间。这包括插入值以创建新序列
  4. 高质量的API

另一方面, Data.Sequence对于数据局部性问题并没有多大帮助,仅适用于有限集合(不如列表慢)

数组不适合胆小的人

数组是CS中最重要的数据结构之一,但它们与惰性纯函数世界并不十分吻合。数组使ϴ(1)可以访问集合的中部,并且可以提供非常好的数据局部性/恒定因素。但是,由于它们不太适合Haskell,因此使用起来很麻烦。当前标准库中实际上有许多不同的数组类型。这些包括完全持久性数组,用于IO monad的可变数组,用于ST monad的可变数组以及上述内容的未盒装版本。欲了解更多信息 haskell Wiki。

向量是一个“更好”的数组

Data.Vector软件包以更高级别和更清洁的API提供了所有阵列的优点。除非您真的知道自己在做什么,否则如果需要类似数组的性能,则应使用它们。当然,仍然有一些注意事项-可变数组(如数据结构)在纯惰性语言中不能很好地发挥作用。不过,有时您仍希望获得O(1)性能,并且 Data.Vector以可用的包装形式提供给您。

您还有其他选择

如果您只希望列表能够在末尾有效地插入,则可以使用差异列表。最好的例子是,列表可能会导致性能下降[Char],而前奏的别名是StringChar列表很方便,但运行速度往往比C字符串慢20倍左右,因此可以随意使用Data.Text或使用它Data.ByteString。我敢肯定,现在我还没有想到其他面向序列的库。

结论

我需要在Haskell列表中进行顺序收集的90%以上的时间是正确的数据结构。列表就像迭代器一样,使用列表的函数可以很容易地与toList它们附带的其他任何数据结构一起使用。在一个更好的世界中,前奏将完全取决于其使用的容器类型的参数,但目前却 []乱丢了标准库。因此,几乎可以在任何地方使用列表(几乎)。
您可以获得大多数列表函数的完全参数化版本(并且很荣幸使用它们)

Prelude.map                --->  Prelude.fmap (works for every Functor)
Prelude.foldr/foldl/etc    --->  Data.Foldable.foldr/foldl/etc
Prelude.sequence           --->  Data.Traversable.sequence
etc

实际上,Data.Traversable定义了一个在“类似于”列表中或多或少通用的API。

尽管如此,尽管您可能会很好并且只编写完全参数化的代码,但我们大多数人都不是,并且在各处使用list。如果您正在学习,我强烈建议您也这样做。


编辑:根据意见,我意识到,我从来没有解释何时使用Data.VectorVS Data.Sequence。数组和向量提供了极快的索引和切片操作,但从根本上讲是瞬态(命令式)数据结构。纯功能性数据结构一样Data.Sequence,并[]让有效地产生新的,如果你修改了旧值从旧值值。

  newList oldList = 7 : drop 5 oldList

不会修改旧列表,也不必复制它。因此,即使oldList时间长得令人难以置信,这种“修改”也会非常快。相似地

  newSequence newValue oldSequence = Sequence.update 3000 newValue oldSequence 

将产生一个新的序列,用newValuefor代替其3000元素。同样,它不会破坏旧序列,只会创建一个新序列。但是,这样做非常有效,采用O(log(min(k(k,kn))),其中n是序列的长度,而k是您修改的索引。

您无法轻松地使用Vectors和进行此操作Arrays。可以修改它们,但这是真正的命令性修改,因此不能用常规的Haskell代码完成。这意味着Vector包中的操作需要进行修改,例如复制snoccons必须复制整个向量,因此需要花费O(n)时间。唯一的例外是,您可以Vector.MutableSTmonad(或IO)中使用可变版本(),并像使用命令式语言一样进行所有修改。完成后,您可以“冻结”向量以将其变成要与纯代码一起使用的不变结构。

我的感觉是,Data.Sequence如果列表不合适,则应默认使用。使用Data.Vector仅当您使用模式不涉及使许多修改,或者如果你需要的ST / IO单子内极高的性能。

如果所有关于STmonad的谈话都让您感到困惑:那就更有理由坚持纯净而又美丽了Data.Sequence


45
我听到的一个见解是,列表基本上与Haskell中的数据结构一样,是一种控制结构。这是有道理的:如果您要使用C语言风格的另一种语言进行循环,则可以使用[1..]Haskell中的列表。列表还可以用于有趣的事情,例如回溯。将它们视为控制结构(某种)确实有助于理解它们的用法。
迪洪·杰维斯

21
极好的答案。我唯一的抱怨是“序列具有功能性”使它们有点卖空。序列是功能强大的工具。他们的另一个好处是快速加入和分裂(登录n)。
丹·伯顿

3
@丹伯顿博览会。我可能确实卖了Data.Sequence。手指树是计算历史上最了不起的发明之一(Guibas也许有一天应该会获得图灵奖),并且Data.Sequence是一种出色的实现,并且具有非常有用的API。
菲利普·JF,2012年

3
“UseData.Vector只有当您的使用模式并不涉及让许多修改,或者如果你需要的ST / IO单子内极高的性能。”有趣的字眼,因为如果你正在做许多改进(如多次(10万次)不断发展的100k元素),那么您确实需要ST / IO矢量才能获得可接受的性能,
misterbee 2012年

4
通过流融合可以部分缓解对(纯)矢量和复制的担忧,例如:import qualified Data.Vector.Unboxed as VU; main = print (VU.cons 'a' (VU.replicate 100 'b'))在Core:hpaste.org/65015
FunctorSalad 2012年
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.