Recursion 如果Clojure中唯一的非堆栈消耗循环构造是;“重复”一词;,这个懒惰的seq是如何工作的?
生成所有正数的惰性序列的步骤:Recursion 如果Clojure中唯一的非堆栈消耗循环构造是;“重复”一词;,这个懒惰的seq是如何工作的?,recursion,clojure,tail-recursion,Recursion,Clojure,Tail Recursion,生成所有正数的惰性序列的步骤: (defn positive-numbers ([] (positive-numbers 1)) ([n] (cons n (lazy-seq (positive-numbers (inc n)))))) 这个惰性seq可以在不抛出StackOverflower错误的情况下针对相当大的索引进行计算(与同一页面上的sieve示例不同): 如果在递归函数中,这个懒惰的seq示例怎么可能在不溢出堆栈的情况下调用自己呢?一个懒惰的序列在一个thunk中有剩余的序
(defn positive-numbers
([] (positive-numbers 1))
([n] (cons n (lazy-seq (positive-numbers (inc n))))))
这个惰性seq可以在不抛出StackOverflower错误的情况下针对相当大的索引进行计算(与同一页面上的sieve示例不同):
如果在递归函数中,这个懒惰的seq示例怎么可能在不溢出堆栈的情况下调用自己呢?一个懒惰的序列在一个thunk中有剩余的序列生成计算。它不是立即调用的。在请求每个元素(或元素块,视情况而定)时,会调用下一个thunk来检索值。如果继续,该thunk可能会创建另一个thunk来表示序列的尾部。神奇之处在于:(1)这些特殊thunk实现了序列接口,可以透明地使用;(2)每个thunk只调用一次——它的值被缓存——因此实现的部分是一个值序列 这里是没有魔法的总体思路,只是好的ol'函数:
(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
,它使用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)))))))
现在让我们做一个缓慢的无限序列来测试缓存行为
(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))))))
和REPL测试
user=> (def nums (slow-pos-nums))
#'user/nums
user=> (time (doall (my-take 10 nums)))
"Elapsed time: 9000.384616 msecs"
(1 2 3 4 5 6 7 8 9 10)
user=> (time (doall (my-take 10 nums)))
"Elapsed time: 0.043146 msecs"
(1 2 3 4 5 6 7 8 9 10)
我认为诀窍在于生产者函数(正数)不会被递归调用,它不会像使用基本的递归Little Schemer样式调用一样累积堆栈帧,因为LazySeq会根据需要为序列中的各个条目调用它。一旦一个闭包被计算为一个条目,那么它就可以被丢弃。因此,当代码在序列中搅动时,来自函数先前调用的堆栈帧可能会被垃圾收集。请记住,
lazy seq
是一个宏,因此在调用正数
函数时不会计算其主体。从这个意义上说,正数
并不是真正的递归。它立即返回,直到seq被使用,对正数的内部“递归”调用才会发生
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)))
我认为您在文档中读得太多了,这句话比实际意义更大:recur
是唯一的非堆栈消耗循环构造,但这并不是说没有它您就不能进行非堆栈消耗递归。不仅可以看到lazy-seq方法,还可以看到trampoline
。注意:这里使用atom
只是为了举例说明。如果您实际上被迫在Clojure中实现这一点,则更合适的方法是延迟。
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)))