Clojure集合的操作

7

我对Clojure还比较陌生,虽然我熟悉函数式语言,主要是Scala。

我正在尝试弄清楚在Clojure中操作集合的惯用方式。我对map等函数的行为特别困惑。

在Scala中,非常注重确保map始终返回与原始集合相同类型的集合,只要这有意义:

List(1, 2, 3) map (2 *) == List(2, 4, 6)
Set(1, 2, 3) map (2 *) == Set(2, 4, 6)
Vector(1, 2, 3) map (2 *) == Vector(2, 4, 6)

相反,在Clojure中,据我了解,大多数操作(如mapfilter)都是惰性的,即使在急切的数据结构上调用也是如此。这会产生奇怪的结果,使得

(map #(* 2 %) [1 2 3])

使用惰性列表而不是向量。虽然我通常更喜欢使用惰性操作,但我发现上述内容令人困惑。事实上,向量保证了某些列表所没有的性能特征。
比如,如果我使用上述结果并在其末尾添加数据。如果我理解正确的话,结果不会被计算,直到我尝试在其上进行添加,然后它才会被计算,并返回一个列表而不是向量;因此,我必须遍历它才能在末尾添加数据。当然,之后我可以把它转换成向量,但这样会变得混乱而且可能被忽视。
如果我理解正确,`map` 是多态的,因此在向量上实现返回向量,在列表上实现返回列表,在流上实现返回流(这次采用惰性方式)等是没有问题的。我想我对 Clojure 的基本设计和惯用法有所误解。
为什么 Clojure 数据结构的基本操作不保留结构呢?

看一下 map 的源代码。Map 不关心集合的类型。你可以在 map 之上构建一个宏,记住集合的类型,并在最后将集合转换为该类型。https://github.com/clojure/clojure/blob/master/src/clj/clojure/core/reducers.clj - Diego Basch
1
请查看 https://github.com/clojure/algo.generic 中的 clojure.algo.generic.functor/fmap,其中包含一个保留输入类型的 map 实现。 - Alex
1个回答

7
在Clojure中,许多函数都基于“Seq(序列)”抽象。这种方法的好处在于,您不需要为每个不同的集合类型编写函数——只要您的集合可以被视为序列(具有头和可能的尾部),就可以将其与所有序列函数一起使用。取seq并输出seq的函数比限制其使用于某种集合类型的函数更具组合性和可重用性。在对seq编写自己的功能时,您不需要处理特殊情况,例如:如果用户给我一个vector,我必须返回一个vector等。您的函数将像任何其他seq函数一样适合于seq管道中。
map返回lazy seq的原因是设计选择。在Clojure中,懒惰是许多这些函数式结构的默认设置。如果您希望拥有其他行为,例如没有中间集合的并行性,请查看reducers库:http://clojure.com/blog/2012/05/08/reducers-a-library-and-model-for-collection-processing.html 就性能而言,map始终必须在集合上应用n次函数,从第一个到最后一个元素,因此其性能始终为O(n)或更差。在这种情况下,向量或列表没有区别。惰性可能会给您带来的好处是,当您只消耗列表的第一部分时。如果您必须在map的输出末尾添加某些内容,则向量确实更有效率。在这种情况下可以使用Clojure 1.4中添加的“mapv”:它接受一个集合并将输出向量。我认为,只有在有非常好的理由时才要担心这些性能优化。大多数情况下不值得。
在此处阅读有关seq(序列)抽象的更多信息:http://clojure.org/sequences Clojure 1.4中添加的另一个返回向量的高阶函数是“filterv”。

1
我不会说列表与向量没有性能差异 - 这取决于您打算如何使用map的结果 - 例如(nth (map #(* 2 %) really-long-vector) 10000) - Alex
@Alex,你是对的,我在你发表评论之前已经改变了我的答案。 - Michiel Borkent
1
另一个要点是创建序列非常便宜;创建向量虽然仍然便宜,但成本显着更高。 map 很客气地执行了低成本操作,并且让您在之后如果需要将其转换为向量。但也有一点:如果您有一个想要进行映射的向量,通常只需要一个序列,而不需要一个向量。 - amalloy
1
@amalloy同意,对向量进行映射通常只需要一个序列。我看到的mapv的主要用途是强制评估函数以捕获动态变量绑定。 - Alex
1
我同意在序列上工作的函数具有可组合性的保证,但这并不与将向量发送到向量相冲突。换句话说,一个多方法可以有一个通用实现,然后特定的实现可以根据需要利用特定的数据类型。我同意这会给库编写者增加负担,但可能会换取更高效的实现和更可预测的返回类型。 - Andrea

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接