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)))