一个惰性序列在一个thunk中生成其余的序列。它不会立即被调用。当每个元素(或一组元素)被请求时,将调用下一个thunk来检索值。如果需要继续生成序列,则该thunk可能创建另一个thunk来表示序列的尾部。这种魔法在于,(1)这些特殊的thunk实现了序列接口,并且可以透明地用作序列;(2)每个thunk只被调用一次--其值被缓存--因此已实现的部分是一系列值。
以下是没有魔法的概念,只使用了好的老函数:
(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
魔法发生在Java中定义的clojure.lang.LazySeq
内部,但我们实际上可以直接在Clojure中执行魔法(为了示例目的而提供的实现),通过在类型上实现接口并使用原子进行缓存。
(deftype MyLazySeq [thunk-mem]
clojure.lang.Seqable
(seq [_]
(if (fn? @thunk-mem)
(swap! thunk-mem (fn [f] (seq (f)))))
@thunk-mem)
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)
recur
是唯一的非堆栈消耗的 循环结构”,但这并不意味着你不能使用非堆栈消耗的递归。请参考懒惰序列和trampoline
的方法。 - Charles Duffy