列出摇滚
到目前为止,Haskell 中顺序数据最友好的数据结构是列表
data [a] = a:[a] | []
列表为您提供 ϴ (1) 缺点和模式匹配。标准库,以及就此而言的前奏,充满了有用的列表函数,这些函数应该会乱七八糟你的代码(foldr
, map
, filter
)。列表是执着的,又名纯功能性,这非常好。 Haskell 列表并不是真正的“列表”,因为它们是共归纳的(其他语言称这些流),所以像
ones :: [Integer]
ones = 1:ones
twos = map (+1) ones
tenTwos = take 10 twos
工作出色。无限的数据结构摇滚。
Haskell 中的列表提供了一个非常类似于命令式语言中的迭代器的接口(因为惰性)。因此,它们被广泛使用是有道理的。
另一方面
列表的第一个问题是索引它们(!!)
需要 ϴ (k) 时间,这很烦人。另外,追加可能会很慢++
,但 Haskell 的惰性求值模型意味着,如果这些发生的话,可以将其视为完全摊销。
列表的第二个问题是它们的数据局部性较差。当内存中的对象没有彼此相邻放置时,实际处理器会产生高常数。所以,在 C++ 中std::vector
比我所知道的任何纯链表数据结构具有更快的“snoc”(将对象放在末尾),尽管这不是持久数据结构,因此不如 Haskell 列表友好。
列表的第三个问题是空间效率较差。一堆额外的指针会增加你的存储空间(按一个常数因子)。
序列具有功能性
Data.Sequence
内部基于手指树(我知道,你不想知道这一点)这意味着它们有一些不错的特性
-
纯功能性。Data.Sequence
是一个完全持久的数据结构。
-
该死的快速访问树的开头和结尾。 ϴ (1)(摊销)获取第一个或最后一个元素,或追加树。在事物列表最快的地方,Data.Sequence
至多是一个恒定的慢速。
-
ϴ (log n) 访问序列的中间部分。这包括插入值以生成新序列
-
高品质API
另一方面,Data.Sequence
对于数据局部性问题没有多大作用,并且仅适用于有限集合(它比列表更懒)
数组不适合胆小的人
数组是 CS 中最重要的数据结构之一,但它们不太适合惰性纯函数世界。数组提供了对集合中间的 ϴ (1) 访问以及非常好的数据局部性/常数因子。但是,由于它们不太适合 Haskell,所以使用起来很痛苦。当前标准库中实际上有多种不同的数组类型。其中包括完全持久化的数组、IO monad 的可变数组、ST monad 的可变数组以及上述的未装箱版本。欲了解更多请查看哈斯克尔维基
矢量是一个“更好”的数组
The Data.Vector
包以更高级别和更简洁的 API 提供了数组的所有优点。除非您真的知道自己在做什么,否则如果您需要类似数组的性能,则应该使用它们。当然,一些警告仍然适用——可变的类似数组的数据结构在纯惰性语言中表现不佳。尽管如此,有时你还是想要 O(1) 的性能,并且Data.Vector
将其以可用的包装形式提供给您。
你还有其他选择
如果您只想要能够在末尾有效插入的列表,您可以使用差异清单。列表搞砸性能的最好例子往往来自[Char]
前奏曲被别名为String
. Char
列表很方便,但运行速度往往比 C 字符串慢 20 倍,所以请随意使用Data.Text
或非常快Data.ByteString
。我确信我现在还没有想到其他面向序列的库。
结论
90+% 的情况下,我需要 Haskell 列表中的顺序集合是正确的数据结构。列表就像迭代器,使用列表的函数可以轻松地与任何其他数据结构一起使用,使用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。
尽管如此,尽管你可以很好地只编写完全参数化的代码,但我们大多数人都不是,并且到处都使用列表。如果你正在学习,我强烈建议你也学习。
根据评论,我意识到我从未解释过何时使用Data.Vector
vs Data.Sequence
。数组和向量提供极快的索引和切片操作,但本质上是瞬态(命令式)数据结构。纯函数式数据结构,例如Data.Sequence
and []
让高效生产new旧值的值,就像您修改了旧值一样。
newList oldList = 7 : drop 5 oldList
不修改旧列表,也不必复制它。所以即使oldList
长得不可思议,这个“修改”会非常快。相似地
newSequence newValue oldSequence = Sequence.update 3000 newValue oldSequence
将产生一个新序列newValue
代替其 3000 个元素。同样,它不会破坏旧的序列,它只是创建一个新的序列。但是,它的效率非常高,采用 O (log (min (k, k-n)),其中 n 是序列的长度,k 是您修改的索引。
你不能轻易做到这一点Vectors
and Arrays
。他们可以modified但这是真正的命令式修改,因此无法在常规 Haskell 代码中完成。这意味着运营Vector
进行修改的包,例如snoc
and cons
必须复制整个向量,所以需要O(n)
时间。唯一的例外是您可以使用可变版本(Vector.Mutable
) 在 - 的里面ST
单子(或IO
)并像使用命令式语言一样进行所有修改。完成后,您可以“冻结”向量,将其转换为您想要与纯代码一起使用的不可变结构。
我的感觉是你应该默认使用Data.Sequence
如果列表不合适。使用Data.Vector
仅当您的使用模式不涉及进行大量修改,或者您需要 ST/IO monad 内的极高性能时。
如果这一切都在谈论ST
monad 让你感到困惑:更有理由坚持纯粹、快速和美丽Data.Sequence
.