(conj collection item) 将item 添加到collection。为此,它需要实现collection。 (我将在下面解释原因。)所以递归调用会立即发生,而不是被延迟。
(cons item collection) 创建一个以item 开头的序列,然后是collection 中的所有内容。值得注意的是,它不需要实现collection。所以递归调用将被推迟(因为使用了lazy-seq),直到有人试图获取结果序列的尾部。
我将在内部解释这是如何工作的:
cons 实际上返回一个clojure.lang.Cons 对象,这就是惰性序列的组成部分。 conj 返回您传递给它的相同类型的集合(无论是列表、向量还是其他)。 conj 使用对集合本身的多态 Java 方法调用来执行此操作。 (见line 524 of clojure/src/jvm/clojure/lang/RT.java。)
当 Java 方法调用发生在 lazy-seq 返回的 clojure.lang.LazySeq 对象上时会发生什么? (Cons 和 LazySeq 对象如何协同工作以形成惰性序列将在下面变得更加清晰。)查看line 98 of clojure/src/jvm/clojure/lang/LazySeq.java。请注意,它调用了一个名为seq 的方法。这就是实现LazySeq 价值的原因(详情请跳转至line 55)。
所以你可以说conj 需要确切地知道你通过了什么样的集合,但cons 不需要。 cons 只要求“集合”参数是 ISeq。
请注意,Clojure 中的Cons 对象与其他 Lisps 中的“cons 单元格”不同——在大多数 Lisps 中,“cons”只是一个包含 2 个指向其他任意对象的指针的对象。所以你可以使用 cons 单元来构建树,等等。 Clojure Cons 将任意 Object 作为头部,将 ISeq 作为尾部。由于Cons 本身实现了ISeq,因此您可以从Cons 对象构建序列,但它们也可以指向向量或列表等。(请注意,Clojure 中的“列表”是一种特殊类型( PersistentList),并且不是从Cons对象构建的。)clojure.lang.LazySeq也实现了ISeq,所以它可以用作尾部(“cdr " 在 Lisps 中) 的 Cons。 LazySeq 包含对某些代码的引用,该代码评估到某种类型的ISeq,但在需要之前它实际上不会评估该代码,并且在它评估代码之后,它会缓存返回的ISeq 并委托给它。
...这一切都开始有意义了吗?你知道惰性序列是如何工作的吗?基本上,您从LazySeq 开始。当LazySeq 实现时,它的计算结果为Cons,它指向另一个LazySeq。当那个被实现时……你明白了。所以你会得到一个LazySeq 对象链,每个对象都持有(并委托给)Cons。
关于 Clojure 中“conses”和“lists”的区别,“lists”(PersistentList 对象)包含一个缓存的“length”字段,因此它们可以在 O(1) 时间内响应 count。这在其他 Lisps 中不起作用,因为在大多数 Lisps 中,“列表”是可变的。但是在 Clojure 中它们是不可变的,因此缓存长度是可行的。
Cons Clojure 中的对象没有 有缓存长度——如果有,它们怎么能用来实现惰性(甚至无限)序列?如果您尝试获取Cons 的count,它只会在其尾部调用count,然后将结果加1。