The ClojureDocs page for lazy-seq
gives an example生成所有正数的lazy-seq:
(defn positive-numbers
([] (positive-numbers 1))
([n] (cons n (lazy-seq (positive-numbers (inc n))))))
这个lazy-seq可以针对相当大的索引进行求值,而不会抛出StackOverflowError(与同一页面上的筛选示例不同):
user=> (nth (positive-numbers) 99999999)
100000000
如果递归函数中有only recur
can be used to avoid consuming stack frames,那么这个lazy-seq示例怎么可能看起来自己调用而不会溢出堆栈呢?
答案 0 :(得分:10)
延迟序列的其余部分在thunk中生成计算。它没有被立即调用。当请求每个元素(或者元素块)时,调用下一个thunk来检索值。如果它继续,那个thunk可能会创建另一个thunk来表示序列的尾部。神奇之处在于:(1)这些特殊的thunk实现了序列接口,并且可以透明地使用它们。(2)每个thunk只被调用一次 - 它的值被缓存 - 所以实现的部分是一系列值。 / p>
这是没有魔力的一般想法,只是好的功能:
(defn my-thunk-seq
([] (my-thunk-seq 1))
([n] (list n #(my-thunk-seq (inc n)))))
(defn my-next [s] ((second s)))
(defn my-realize [s n]
(loop [a [], s s, n n]
(if (pos? n)
(recur (conj a (first s)) (my-next s) (dec n))
a)))
user=> (-> (my-thunk-seq) first)
1
user=> (-> (my-thunk-seq) my-next first)
2
user=> (my-realize (my-thunk-seq) 10)
[1 2 3 4 5 6 7 8 9 10]
user=> (count (my-realize (my-thunk-seq) 100000))
100000 ; Level stack consumption
魔术位发生在Java中定义的clojure.lang.LazySeq
内部,但我们实际上可以直接在Clojure中实现魔术(实现后面的实现),通过在类型上实现接口并使用atom来缓存
(deftype MyLazySeq [thunk-mem]
clojure.lang.Seqable
(seq [_]
(if (fn? @thunk-mem)
(swap! thunk-mem (fn [f] (seq (f)))))
@thunk-mem)
;Implementing ISeq is necessary because cons calls seq
;on anyone who does not, which would force realization.
clojure.lang.ISeq
(first [this] (first (seq this)))
(next [this] (next (seq this)))
(more [this] (rest (seq this)))
(cons [this x] (cons x (seq this))))
(defmacro my-lazy-seq [& body]
`(MyLazySeq. (atom (fn [] ~@body))))
现在这已经适用于take
等,但是当take
调用lazy-seq
时,我们会创建一个使用my-take
的{{1}}代替任何困惑。
my-lazy-seq
现在让我们制作一个缓慢的无限序列来测试缓存行为。
(defn my-take
[n coll]
(my-lazy-seq
(when (pos? n)
(when-let [s (seq coll)]
(cons (first s) (my-take (dec n) (rest s)))))))
和REPL测试
(defn slow-inc [n] (Thread/sleep 1000) (inc n))
(defn slow-pos-nums
([] (slow-pos-nums 1))
([n] (cons n (my-lazy-seq (slow-pos-nums (slow-inc n))))))
答案 1 :(得分:4)
请注意,lazy-seq
是一个宏,因此在调用positive-numbers
函数时不评估其正文。从这个意义上说,positive-numbers
并不是真正的递归。它立即返回,并且在seq被消耗之前不会发生对positive-numbers
的内部“递归”调用。
user=> (source lazy-seq)
(defmacro lazy-seq
"Takes a body of expressions that returns an ISeq or nil, and yields
a Seqable object that will invoke the body only the first time seq
is called, and will cache the result and return it on all subsequent
seq calls. See also - realized?"
{:added "1.0"}
[& body]
(list 'new 'clojure.lang.LazySeq (list* '^{:once true} fn* [] body)))
答案 2 :(得分:1)
我认为诀窍是生成器函数(正数)不是递归调用的,它不会累积堆栈帧,好像它是用基本递归Little-Schemer样式调用的,因为LazySeq根据需要为序列中的各个条目调用它。一旦为一个条目评估一个闭包,就可以将其丢弃。因此,当代码在序列中进行搅动时,来自函数先前调用的堆栈帧可以被垃圾收集。