Clojure中序列和集合的区别是什么?

39

我是一名Java程序员,新接触Clojure。在不同的地方看到序列(sequence)和集合(collection)在不同情况下被使用。然而,我不知道它们之间的确切区别。

以下是一些例子:

1) 在Clojure的Sequence文档中:

The Seq interface
(first coll)
  Returns the first item in the collection. 
  Calls seq on its argument. If coll is nil, returns nil.
(rest coll)
  Returns a sequence of the items after the first. Calls seq on its argument. 
  If there are no more items, returns a logical sequence for which seq returns nil.
(cons item seq)
  Returns a new seq where item is the first element and seq is the rest.

如您所见,当描述Seq接口时,前两个函数(first/rest)使用coll,这似乎表明这是一个集合,而cons函数使用seq,这似乎表明这是一个序列。

2) 有名为coll?seq?的函数,可以用于测试值是否为集合或序列。很显然,集合和序列是不同的。

3) 在Clojure的“集合”文档中,它说:

  

因为集合支持seq函数,所以任何集合都可以与所有序列函数一起使用

这是否意味着所有集合都是序列?

(coll? [1 2 3]) ; => true 
(seq? [1 2 3]) ; => false

上面的代码告诉我这不是这种情况,因为[1 2 3]是一个集合而不是一个序列。

我认为这对于Clojure来说是一个相当基础的问题,但是我找不到一个清晰地解释它们之间差异以及在不同情况下应该使用哪个的地方。欢迎任何评论。


1
最大的困惑源于 seq? 并不测试一个“东西”是否是一个“序列”(或者更确切地说,一个“顺序集合”),而是它是否是一个 seq(也就是可以通过 firstnextrest 操作的东西,严格来说是一个 clojure.lang.PersistentVector$ChunkedSeq)。观察:(def x [1 2]),然后:(coll? x) => true,(sequential? x) => true,(seq? x) => false,但是!(seq? (seq x)) => true。 - David Tonhofer
6个回答

27
任何支持核心 firstrest 函数的对象都是一个序列(sequence)
许多对象满足此接口,每个Clojure集合都提供至少一种seq对象,以使用seq函数遍历其内容。
所以:
user> (seq [1 2 3])
    (1 2 3)

你也可以从一个map创建一个序列对象。

user> (seq {:a 1 :b 2})
    ([:a 1] [:b 2])

这就是为什么你可以在mapssets等对象上使用filtermapfor等函数。因此,你可以将许多类似集合的对象视为序列。
这也是为什么许多序列处理函数(如filter)会调用输入的seq函数。
 (defn filter
  "Returns a lazy sequence of the items in coll for which
  (pred item) returns true. pred must be free of side-effects."
  {:added "1.0"
   :static true}
  ([pred coll]
   (lazy-seq
      (when-let [s (seq coll)]

如果您调用(filter pred 5)
  Don't know how to create ISeq from: java.lang.Long
                  RT.java:505 clojure.lang.RT.seqFrom
                  RT.java:486 clojure.lang.RT.seq
                 core.clj:133 clojure.core/seq
                core.clj:2523 clojure.core/filter[fn]

你会发现seq调用是验证这个对象是否为序列的方法。
如果你想深入了解,大部分内容都在Clojure之乐第5章中。

1
你还需要支持cons操作成为一个序列。 - Luke De Feo
1
我已经为此付出了足够的汗水。《Clojure之乐》第5章需要重写。 - David Tonhofer
任何支持核心first和rest函数的对象都是序列。First和rest可以在[1 2 3]上工作,但在seq?上却返回false。 (seq?[1 2 3]) ; => false (first [1 2 3]) ; => 1 (rest [1 2 3]) ; => (2 3) - slk500

22
以下是关于“集合”和“序列”的区别的几点说明:
  1. “集合”和“序列”是抽象的概念,不能从给定的值中确定其属性。

  2. 集合是一组值的无序组合。

  3. 序列是一种数据结构(集合的子集),期望按顺序(线性方式)访问它们。

下面的图形最好地描述了它们之间的关系: enter image description here 您可以在这里阅读更多相关信息:here

Venn图中将列表向量视为序列。然而(seq? [1])将返回false,正如文章本身所示。 - defhlt
1
@defhlt, (seq? x) 用于检查 x 是否实现了 ISeq 接口。它不是用来检查 x 是否为序列的。欲了解更多信息,请访问 https://clojuredocs.org/clojure.core/seq_q - divyum
好的,谢谢你。我也觉得这个答案很有用:https://dev59.com/0GEh5IYBdhLWcg3waC1t#22439707 - defhlt

16
勇敢而真实的Clojure中,作者以非常易于理解的方式总结了以下内容:

集合抽象与序列抽象密切相关。 Clojure的所有核心数据结构-向量、映射、列表和集合-都参与了这两种抽象。

这些抽象之间的区别在于序列抽象“关注”单个成员的操作,而集合抽象则“关注”整个数据结构。例如,集合函数countempty?every?不是关于任何单个元素的; 它们关注整体。


所有Clojure的核心数据结构——向量、映射、列表和集合——都参与了这两个抽象概念。但实际上这是错误的,因为“映射”和“集合”并不是“序列”。即使对于有序的种类,sequential?也会返回false。 - David Tonhofer

16

每个序列都是一个集合,但并非每个集合都是序列。

seq函数使得将集合转换为序列成为可能。例如,对于一个映射,您可以获得其条目的列表。然而,那个条目列表与映射本身是不同的。


1
有趣的是,在C#中情况恰好相反,每个集合都是一个序列(它实现了IEnumerable),但并不是每个序列都是一个集合。 - Tim Schmelter
@TimSchmelter 这与Clojure是否相同取决于哪个Clojure Java接口被认为等同于.NET的IEnumerable,即SeqableISeq。Clojure集合实现了IPersistentCollection,它是Seqable的子类型,而Seqable所需的唯一函数是我在我的答案中提到的seq。只有ISeq的实现(其本身是IPersistentCollection的子类型)被认为是Clojure中的序列,但可以认为Seqable具有类似于.NET的IEnumerable的作用。 - Rörd

7
我刚刚阅读了《Clojure之乐》第五章“集合类型”,但有些地方令人困惑(即该书的下一版需要审查)。在第五章的第86页,有一张表格,我对它不是完全满意:

Table 5.1 from the Joy of Clojure, 2nd ed.

以下是我的观点(在反思一个月后完全更新)。

集合

它是一个“东西”,是其他东西的集合。

这是基于函数coll?

  • 函数coll?可用于测试此功能。
  • 相反,任何coll?返回true的内容都是集合。

coll? Docstring说:

如果x实现了IPersistentCollection,则返回true

被归为三个不同类别的集合。不同类别中的物品永远不会相等。

  • Maps测试使用(map? foo)
    • Map(两个实际实现,行为略有不同)
    • 排序的映射。注意:(sequential? (sorted-map :a 1)) ;=> false
  • Sets测试使用(set? foo)
    • Set
    • 排序的集合。注意:(sequential? (sorted-set :a :b)) ;=> false
  • Sequential collections测试使用(sequential? foo)
    • List
    • Vector
    • Queue
    • Seq:(sequential? (seq [1 2 3])) ;=> true
    • Lazy-Seq:(sequential? (lazy-seq (seq [1 2 3]))) ;=> true

Java交互部分不在此范围内:

  • (coll? (to-array [1 2 3])) ;=> false
  • (map? (doto (new java.util.HashMap) (.put "a" 1) (.put "b" 2))) ;=> false

序列集合(一种“链”)

它是一个“东西”,一个按照特定、稳定的顺序保存其他东西的集合。

这基于函数sequential?

  • 函数sequential?可用于测试此内容。
  • 相反,任何返回true的sequential?都是一个序列集合。

sequential?文档字符串如下:

如果coll实现了Sequential,则返回true

注意:“sequential”是一个形容词!在《Clojure之乐》中,形容词被用作名词,这真的非常令人困惑:

“Clojure将每个集合数据类型分为三个逻辑类别或分区:序列集合、映射和集合。”

代替“sequential”,应该使用“sequential thing”或“sequential collection”(如上所述)。另一方面,在mathematics中已经存在以下单词:“chain”,“totally ordered set”,“simply ordered set”,“linearly ordered set”。 “chain”听起来很好,但没有人使用这个词。可惜!
《Clojure之乐》也有这样的说法:
注意基于类型的谓词!Clojure包括一些谓词,其名称类似于刚才定义的单词。虽然它们并不经常使用,但似乎值得提到的是,它们可能并不完全意味着这里可能建议的定义。例如,对于每个使sequential?返回true的对象,都是一个sequential collection,但对于一些也是sequential的对象,它返回false [better: "that can be considered sequential collections"]。这是由于实现细节,可能在未来版本的Clojure中得到改进[也许已经完成了?]
序列(也称为“序列抽象”)
这更像是一个概念而非一件事物:一系列值(因此有序),可能存在也可能不存在(即流)。如果你说一个东西是一个序列,那么这个东西是否必须是一个Clojure集合,甚至是一个连续的集合?我想是的。
这个连续的集合可以完全计算并完全可用。或者它可以是一个“机器”,根据需要生成值(通过计算 - 可能以“纯”的方式 - 或通过查询外部“不纯”的、“神谕”的来源:键盘、数据库)。
seq
这是一个东西:可以由函数firstrestnextcons(可能还有其他函数?)处理的东西,即遵循protocolclojure.lang.ISeq(与Java中“为接口提供实现”的概念相同)的系统已经注册了函数实现对于一对(thing,function-name) [我真心希望我理解正确...]。
这基于函数seq?
  • 函数seq?可用于测试此内容
  • 反之,序列是任何seq?返回true的东西。

seq?的文档字符串:

如果x实现ISeq,则返回true

first的文档字符串:

返回集合中的第一个项目。在其参数上调用seq。 如果coll为nil,则返回nil。

rest的文档字符串:

返回第一个后面的可能为空的项。在其参数上调用seq。

next的文档字符串:

返回第一个后面的项的seq。在其参数上调用seq。 如果没有更多的项,则返回nil。

您可以在序列上调用next以生成下一个元素和新序列。重复此过程,直到获取nil

Joy of Clojure将其称为“用于导航集合的简单API”,并表示“seq是实现seq API的任何对象”-如果“API”是“事物”(某种类型)和在该事物上工作的函数的整体,则这是正确的。这取决于API概念上的适当转变。

关于空序列的特殊情况的说明:

(def empty-seq (rest (seq [:x])))

(type? empty-seq)                 ;=> clojure.lang.PersistentList$EmptyList

(nil? empty-seq)                  ;=> false ... empty seq is not nil
(some? empty-seq)                 ;=> true ("true if x is not nil, false otherwise.")

(first empty-seq)                 ;=> nil   ... first of empty seq is nil ("does not exist"); beware confusing this with a nil in a nonempty list!
(next empty-seq)                  ;=> nil   ... "next" of empty seq is nil
(rest empty-seq)                  ;=> ()    ... "rest" of empty seq is the empty seq
   (type (rest empty-seq))        ;=> clojure.lang.PersistentList$EmptyList
   (seq? (rest empty-seq))        ;=> true
   (= (rest empty-seq) empty-seq) ;=> true

(count empty-seq)                 ;=> 0
(empty? empty-seq)                ;=> true

附录

函数seq

如果你将函数seq应用于一个有意义的东西(通常是一个序列集合),你会得到一个代表/生成该集合成员的序列。

文档字符串如下:

返回一个集合的序列。如果集合为空,则返回nil。(seq nil)返回nil。seq还适用于字符串、本地Java数组(引用类型)和任何实现Iterable的对象。请注意,seq缓存值,因此不应在其迭代器重复返回相同可变对象的任何Iterable上使用seq。

应用seq后,您可能会获得各种实际类的对象:

  • clojure.lang.Cons - 尝试 (class (seq (map #(* % 2) '( 1 2 3))))
  • clojure.lang.PersistentList
  • clojure.lang.APersistentMap$KeySeq
  • clojure.lang.PersistentList$EmptyList
  • clojure.lang.PersistentHashMap$NodeSeq
  • clojure.lang.PersistentQueue$Seq
  • clojure.lang.PersistentVector$ChunkedSeq

如果将seq应用于一个序列,则返回的东西的实际类可能与传入的实际类不同。它仍然是一个序列。

序列中的“元素”取决于对象类型。例如,对于映射,它们是看起来像2元素向量的键值对(但它们的实际类并不是真正的向量)。

lazy-seq函数

创建一个生成更多物品的懒加载的东西(一个暂停的机器,一个暂停的流,一个 thunk
文档字符串如下:
接受一个返回 ISeq 或 nil 的表达式列表,并产生一个 Seqable 对象,该对象在第一次调用 seq 时仅调用表达式一次,并将结果缓存并在所有后续的 seq 调用中返回。另请参见-realized?"。
关于“函数”和“物品”...以及“对象”的注释
在 Clojure Universe 中,我喜欢谈论“函数”和“物品”,但不是“对象”,因为这个术语充满了 Java-ness 和其他糟糕的东西。提到对象感觉像是从底层的Java universe中突出的碎片。
函数和物品之间有什么区别?
它是流动的!有些东西是纯函数,有些东西是纯物品,有些则介于两者之间(可以用作函数并具有物品的属性)。

特别地,Clojure允许将关键字(things)视为函数(在映射中查找值),或将映射(things)解释为函数,或缩写为函数(它们接受一个键并返回映射中与该键相关联的值)。

显然,函数作为"first-class citizens"也是一种thing。

这也是上下文相关的!在某些情况下,函数变成了一个thing,或者一个thing变成了一个函数。

有关对象的不良提及...这些是从底层Java世界中突出的碎片。

为了演示目的,附上集合的图表

Collections in Clojure


排序映射。注意:(sequential?(sorted-map:a 1);=> false`,此行缺少一个括号。 顺便说一句,这个答案非常清晰。谢谢! - SoulerTsai

3
对于 seq?:

如果x实现了ISeq,则返回true

对于 coll?:

如果x实现了IPersistentCollection,则返回true

我在Clojure源代码中发现ISeq接口扩展自IPersistentCollection,因此正如Rörd所说,每个序列都是集合。

@Jerry101 很高兴你想要为这篇帖子增加内容,但是建议修改并不是正确的方式。要提出改进意见,最好是添加一条评论来解释需要修复或更新的地方。另外,请不要多次建议对同一篇帖子进行相同的修改。 - Undo

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