如果Clojure中唯一没有堆栈消耗的循环结构是" recur",这个lazy-seq是如何工作的?

时间:2014-03-11 14:12:39

标签: recursion clojure tail-recursion

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示例怎么可能看起来自己调用而不会溢出堆栈呢?

3 个答案:

答案 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根据需要为序列中的各个条目调用它。一旦为一个条目评估一个闭包,就可以将其丢弃。因此,当代码在序列中进行搅动时,来自函数先前调用的堆栈帧可以被垃圾收集。