Clojure中的cons和conj与lazy-seq有什么区别?

26

为什么在这个具有lazy-seq的上下文中,cons有效而conj无效?

以下代码有效:

(defn compound-interest [p i]
   (cons p (lazy-seq (compound-interest (* p (+ 1 i)) i))))

这样写会导致堆栈溢出异常:

(defn compound-interest2 [p i]
   (conj (lazy-seq (compound-interest2 (* p (+ 1 i)) i)) p))

1
对于其他来到这里的人:我的下面答案非常详细(可能太详细),可能会令人困惑。让我再重申一遍主要观点:'conj' 的语义要求根据集合类型变化其行为。这需要在集合对象上使用多态方法调用。'LazySeq' 处理该方法调用通过委托其内部值来实现,而这需要实现该内部值。相反,cons 的语义不要求在集合上调用任何方法;它只需将其存储在 'Cons' 对象的一个字段中。 - Alex D
答案的最后三段尤其有帮助。一些人可能想先阅读它们。 - Mars
1个回答

47

(conj collection item)item 添加到 collection 中。为了实现这个过程,它需要先实现 collection。(下面我会解释为什么)所以递归调用会立即发生,而不是被延迟。

(cons item collection) 创建一个序列,该序列以 item 开始,后跟 collection 中的所有内容。重要的是,它不需要 实现 collection。因此,递归调用将被延迟(因为使用了 lazy-seq),直到有人尝试获取结果序列的尾部。

我将解释内部工作原理:

cons 实际上返回一个 clojure.lang.Cons 对象,这就是惰性序列的构成。conj 返回与传入的集合相同类型的集合(无论是列表、向量还是其他任何类型)。conj 使用集合本身的多态 Java 方法调用来实现这一点。(请参见 clojure/src/jvm/clojure/lang/RT.java 的第 524 行。)

当对由lazy-seq返回的clojure.lang.LazySeq对象进行Java方法调用时会发生什么?(如何将ConsLazySeq对象结合起来形成惰性序列将在下面变得更加清晰。)请查看clojure/src/jvm/clojure/lang/LazySeq.java第98行。注意它调用了一个名为seq的方法。这就是实现LazySeq的值的过程(跳转到第55行查看详细信息)。
因此,可以说conj需要确切知道您传递给它的集合类型,但cons不需要。 cons只需要“collection”参数是一个ISeq即可。请注意,在Clojure中,Cons对象与其他Lisp中的“cons单元”是不同的——在大多数Lisp中,“cons”只是一个包含指向其他任意对象的2个指针的对象。因此,您可以使用cons单元来构建树等结构。Clojure的Cons将任意的Object作为头部,并将ISeq作为尾部。由于Cons本身实现了ISeq,因此您可以使用Cons对象构建序列,但它们也可以指向向量、列表等。请注意,在Clojure中,“list”是一种特殊类型(PersistentList),不是由Cons对象构建的。clojure.lang.LazySeq也实现了ISeq,因此它可以用作Cons的尾部(Lisps中的“cdr”)。LazySeq持有对某些代码的引用,该代码评估为某种类型的ISeq,但直到需要时才会实际评估该代码,并且在评估代码之后,它缓存返回的ISeq并委托给它。
这一切开始让你有所理解了吗?你明白了惰性序列的工作原理吗?基本上,您从一个LazySeq开始。当实现LazySeq时,它会评估为一个指向另一个LazySeqCons。当那个被实现时……你明白了吧。因此,您获得了一系列LazySeq对象,每个对象都持有(并委托给)一个Cons。关于Clojure中的"conses"和"lists"之间的区别,"lists"(PersistentList对象)包含一个缓存的"length"字段,因此它们可以在O(1)时间内响应count。这在其他Lisp中不起作用,因为在大多数Lisp中,"lists"是可变的。但在Clojure中,它们是不可变的,因此缓存长度是有效的。
在Clojure中,Cons对象没有缓存的长度 - 如果它们有缓存的长度,如何将其用于实现惰性(甚至无限)序列呢?如果您尝试获取Conscount,它只会调用其尾部上的count,然后将结果加1。

1
因为 conj 适用于所有 seqs(不仅仅是列表),所以它做出了这样的总体假设:集合必须被实现(如果它是一个向量,那么这是必须的)。理论上,如果集合是一个列表,conj 可能会像 cons 一样工作,对吧?(或者我有什么遗漏吗?) - caleb
1
如果 conj 只能用于列表,那么你也无法在 LazySeq 上使用它,所以整个问题就变得无关紧要了。基本上,问题归结为:conj 的语义要求它根据集合类型变化其行为。这需要在集合对象上使用(多态)方法调用。LazySeq 通过委托给其内部值来处理该方法调用,这需要实现该内部值。相比之下,cons 的语义不要求在集合上调用任何方法;它只需将其存储在 Cons 的一个字段中即可。 - Alex D
2
很棒的答案。除此之外,它还回答了另一个问题:如果Clojure已经有PersistentLists,其中包含Cons'es缺少的长度字段,为什么还需要Cons? - Mars
@Mars,这不是因为缺少长度字段允许惰性评估吗?只是从我读到的答案中得出的结论。 - Rob Grant
"...这样它们可以在O(1)时间内响应(计数)。"---你确定吗? - 象嘉道
显示剩余4条评论

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