为什么在这个具有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))
为什么在这个具有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))
(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方法调用时会发生什么?(如何将Cons
和LazySeq
对象结合起来形成惰性序列将在下面变得更加清晰。)请查看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
时,它会评估为一个指向另一个LazySeq
的Cons
。当那个被实现时……你明白了吧。因此,您获得了一系列LazySeq
对象,每个对象都持有(并委托给)一个Cons
。关于Clojure中的"conses"和"lists"之间的区别,"lists"(PersistentList
对象)包含一个缓存的"length"字段,因此它们可以在O(1)时间内响应count
。这在其他Lisp中不起作用,因为在大多数Lisp中,"lists"是可变的。但在Clojure中,它们是不可变的,因此缓存长度是有效的。Cons
对象没有缓存的长度 - 如果它们有缓存的长度,如何将其用于实现惰性(甚至无限)序列呢?如果您尝试获取Cons
的count
,它只会调用其尾部上的count
,然后将结果加1。conj
适用于所有 seqs(不仅仅是列表),所以它做出了这样的总体假设:集合必须被实现(如果它是一个向量,那么这是必须的)。理论上,如果集合是一个列表,conj
可能会像 cons
一样工作,对吧?(或者我有什么遗漏吗?) - calebconj
只能用于列表,那么你也无法在 LazySeq
上使用它,所以整个问题就变得无关紧要了。基本上,问题归结为:conj
的语义要求它根据集合类型变化其行为。这需要在集合对象上使用(多态)方法调用。LazySeq
通过委托给其内部值来处理该方法调用,这需要实现该内部值。相比之下,cons
的语义不要求在集合上调用任何方法;它只需将其存储在 Cons
的一个字段中即可。 - Alex D