clojure中的每个集合都被称为“sequable”,但只有列表和缺点实际上是seqs:
user> (seq? {:a 1 :b 2})
false
user> (seq? [1 2 3])
false
所有其他seq functions首先将集合转换为序列,然后才对其进行操作。
user> (class (rest {:a 1 :b 2}))
clojure.lang.PersistentArrayMap$Seq
我做不到的事情:
user> (:b (rest {:a 1 :b 2}))
nil
user> (:b (filter #(-> % val (= 1)) {:a 1 :b 1 :c 2}))
nil
并且必须强制回到具体的数据类型。这对我来说看起来很糟糕,但很可能我还没有得到它。
那么,为什么clojure集合不直接实现ISeq接口而且所有seq函数都不返回与输入对象相同的类的对象?
答案 0 :(得分:16)
这已在Clojure谷歌小组讨论过;例如,从今年2月开始查看帖子map semantics。我会冒昧地将我在留言中提出的一些要点重新用于下面的那个帖子,同时添加几个新的。
在我继续解释为什么我认为"单独的seq"设计是正确的,我想指出,对于你真正想要输出类似于输入但没有明确的输出的情况的自然解决方案存在于函数{{1来自contrib库algo.generic。 (我不认为默认情况下使用它是个好主意,但是,核心库设计是一个很好的原因。)
我认为关键的观察是,fmap
,map
等序列操作在概念上分为三个不同的关注点:
迭代输入的某种方式;
将函数应用于输入的每个元素;
产生输出。
显然,如果我们可以处理1.和3,那就没有问题。所以让我们来看看那些。
对于1.,请考虑迭代集合的最简单且最高效的方法通常不涉及分配与集合相同的抽象类型的中间结果。将一个函数映射到一个chunked seq上的一个函数可能比一个函数映射一个seq生成"视图向量"更高效。 (使用filter
)每次调用subvec
;然而,后者是我们在Clojure样式向量上next
表现最好的表现(即使在RRB trees的情况下,当我们需要适当的子向量/向量切片操作时,这是很好的实现一个有趣的算法,但如果我们使用它们来实现next
),那么让遍历变得很慢。
在Clojure中,专门的seq类型维护遍历状态和额外功能,例如(1)有序映射和集合的节点堆栈(除了更好的性能之外,这比使用next
/的遍历具有更好的大O复杂度dissoc
!),(2)当前索引+逻辑,用于将叶子数组包装为向量的块,(3)遍历"继续"用于哈希映射。通过这样的对象遍历集合比遍历disj
/ subvec
/ dissoc
的任何尝试都快。
但是,假设我们在将函数映射到向量时愿意接受性能损失。好吧,让我们现在尝试过滤:
disj
这里存在一个问题 - 从矢量中删除元素没有好办法。 (同样,RRB树在理论上可能有所帮助,但在实践中,所有RRB切片和连接都涉及产生"真实矢量"用于过滤操作将绝对破坏性能。)
这是一个类似的问题。考虑这个管道:
(->> some-vector (map f) (filter p?))
在这里,我们可以从懒惰中受益(或者更确切地说,从早期停止过滤和映射的能力中获益;这里有一个涉及减速器的点,见下文)。显然(->> some-sorted-set (filter p?) (map f) (take n))
可以使用take
进行重新排序,但不能使用map
进行重新排序。
关键是如果filter
可以隐式转换为seq,那么filter
也可以。并且可以对其他序列函数进行类似的参数。一旦我们为所有人 - 或几乎所有人 - 提出论证,很明显,map
返回专门的seq
对象也是有意义的。
顺便说一句,过滤或映射集合上的函数而不产生类似的集合是非常有用的。例如,我们通常只关心将转换管道产生的序列减少到某个值或者调用每个元素的副作用函数的结果。对于这些场景,通过维护输入类型并没有任何东西可以获得,并且在性能上会有很多损失。
如上所述,我们并不总是希望生成与输入相同类型的输出。但是,当我们这样做时,通常最好的方法是将输入的seq倾注到空输出集合中。
事实上,绝对没有办法为地图和集合做得更好。基本原因是,对于大于1的基数集,没有办法预测函数在集合上的映射输出的基数,因为函数可以“粘合在一起”。 (产生相同的输出)任意输入。
此外,对于有序地图和集合,无法保证输入集合的比较器能够处理来自任意函数的输出。
所以,如果在很多情况下,没有办法比seq
明显好于单独执行map
和seq
,并考虑两者{{1} }和into
自己创造有用的原语,Clojure选择公开有用的原语并让用户组成它们。这样,我们seq
和into
就可以从一个集合中生成一个集合,同时让我们可以自由地不进入map
阶段通过生成集合(或其他集合类型,视情况而定)获得的值。
在使用reducer时,在映射,过滤等时使用集合类型本身的一些问题不适用。
reducers和seqs之间的关键区别在于into
和朋友生成的中间对象只产生"描述符"在减少器实际减少的情况下,维护有关需要执行哪些计算的信息的对象。因此,可以合并计算的各个阶段。
这使我们可以做像
这样的事情into
当然我们仍然需要明确我们的clojure.core.reducers/map
,但这只是一种说法"减速器管道在这里结束;请以集合的形式生成结果"。我们也可以要求一个不同的集合类型(可能是结果的向量;请注意,在集合上映射(require '[clojure.core.reducers :as r])
(->> some-set (r/map f) (r/filter p?) (into #{}))
可能会产生重复的结果,我们可能在某些情况下希望保留它们)或标量值({{ 1}})。
主要观点如下:
迭代集合的最快方法通常不涉及产生类似于输入的中间结果;
(into #{})
使用最快的方式进行迭代;
通过映射或过滤转换集合的最佳方法涉及使用f
样式的操作,因为我们希望在累积输出时非常快速地迭代;
因此(reduce + 0)
是一个很好的原语;
seq
和seq
,根据具体情况选择处理seqs,可以避免没有上升的性能损失,受益于懒惰等,但仍可用于使用seq
;
因此他们也制作了很好的原语。
其中一些观点可能不适用于静态类型语言,但当然Clojure是动态的。另外,当我们想要一个与输入类型匹配的回报时,我们只是被迫明确表示它,并且这本身可能被视为一件好事。
答案 1 :(得分:9)
序列是逻辑列表抽象。它们提供对(稳定的)有序值序列的访问。它们在集合上实现为视图(具体接口与逻辑接口匹配的列表除外)。序列(视图)是一个单独的数据结构,它引用集合以提供逻辑抽象。
序列函数(map,filter等)采用" seqable" thing(可以生成序列的东西),调用seq来生成序列,然后对该序列进行操作,返回一个新序列。您是否需要或如何将该序列重新收集到具体集合中取决于您。虽然向量和列表是有序的,但是集合和映射不是,因此这些数据结构上的序列必须计算并保留集合外的顺序。
像mapv,filterv,reduce-kv这样的专业功能可以让你在集合中保持""当你知道你希望操作在结尾而不是序列时返回一个集合。
答案 2 :(得分:2)
Seqs是有序结构,而地图和集合是无序的。两个值相等的映射可能具有不同的内部排序。例如:
user=> (seq (array-map :a 1 :b 2))
([:a 1] [:b 2])
user=> (seq (array-map :b 2 :a 1))
([:b 2] [:a 1])
要求rest
地图是没有意义的,因为它不是顺序结构。一套也是如此。
那么矢量呢?它们按顺序排序,因此我们可以映射到一个向量,实际上有这样一个函数:mapv
。
您可能会问:为什么这不是隐含的?如果我将向量传递给map
,为什么它不返回向量?
嗯,首先,这意味着对像向量这样的有序结构进行例外处理,而Clojure在制作例外方面并不重要。
但更重要的是,你失去了seqs最有用的属性之一:懒惰。将map
和filter
等seq函数链接在一起是一种非常常见的操作,如果没有懒惰,这将会降低性能,并且会占用大量内存。
答案 3 :(得分:0)
集合类遵循工厂模式,即不是实现ISeq
,而是实现Sequable
,即可以从集合中创建一个ISeq
,但集合本身不是ISeq
。
现在即使这些集合直接实现ISeq
,我也不确定如何解决你的通用序列函数返回原始对象的问题,因为这些通用目的根本就没有意义函数应该适用于ISeq
,他们不知道哪个对象给了他们ISeq
java中的示例:
interface ISeq {
....
}
class A implements ISeq {
}
class B implements ISeq {
}
static class Helpers {
/*
Filter can only work with ISeq, that's what makes it general purpose.
There is no way it could return A or B objects.
*/
public static ISeq filter(ISeq coll, ...) { }
...
}