Clojure懒序列的使用

43

我不太理解如何在Clojure中创建一个lazy sequence。

这个宏的文档对我来说一点都不清楚:

用法:(lazy-seq & body) 接收一系列返回ISeq或nil的表达式,并产生一个Seqable对象,只有在第一次调用seq时才会执行这些表达式,将结果缓存并在随后的所有 seq调用中返回。

我看到的所有例子,似乎都做了类似以下的事情:

; return everything in the sequence starting at idx n
(defn myseq-after-n [n]
  (...)
)

(def my-lazy-seq
  (lazy-seq (conj [init-value] (myseq-after-n 2)))
)

所以,我不理解的第一件事是,既然lazy-seq在conj调用外部,它如何防止在评估时生成无限序列?

我的第二个问题是,懒惰序列定义是否总是采用这个一般形式?

1个回答

57

lazy-seq调用只会在第一次被访问时执行一次主体,然后缓存并返回相同的结果,无论将来何时再次调用。

如果你想使用它来构建长(甚至是无限)序列,则需要在返回的序列中递归嵌套其他lazy-seq调用。这是我能想到的最简单的情况:

(defn ints-from [n]
  (cons n (lazy-seq (ints-from (inc n)))))

(take 10 (ints-from 7))
=> (7 8 9 10 11 12 13 14 15 16)
任何 (ints-from n) 调用都会生成以 n 开头的序列,随后是一个惰性序列 (ints-from (inc n))。这是一个无限列表,但这并不是问题,因为 lazy-seq 确保只有在需要时才调用 (int-from (inc n))。如果你尝试删除 lazy-seq 并运行相同的代码,你很快就会得到 StackOverflowError。 lazy-seq 只是创建惰性序列的众多可能方法之一,而且它通常不是最方便的。以下是一些其他有趣/有用的创建惰性序列的方式:
; range is an easy way to get an infinite lazy sequence of integers, starting with zero     
(take 10 (range))
=> (0 1 2 3 4 5 6 7 8 9)

; map produces lazy sequences, so the following is lazy 
(take 10 (map #(* % %) (range)))
=> (0 1 4 9 16 25 36 49 64 81)

; iterate is a good way of making infinite sequenes of the form x, f(x), f(f(x))..... 
(take 10 (iterate (partial * 2) 1))
=> (1 2 4 8 16 32 64 128 256 512)

1
懒序列不应该在cons外面吗: (defn from [n] (lazy-seq (cons n (from (+ 1 n))))) ? - andrew cooke
8
@andrew: 不管你先放哪个,其实都没什么关系。把 cons 放在前面会稍微更有效率一点,因为这样第一个对象就是 cons,而不是包含 cons 的 lazy-seq。这样可以使用一个更少的对象……不过影响并不大! - mikera
6
取决于情况,这可能相当重要。使用此代码,您正在热切地评估第一个成员。在您的情况下没有问题,但并非总是如此。 - Marko Topolnik
嗨,我只是想了解一下Clojure中的lazy-seqs是如何工作和使用的,这个答案仍然适用吗?你能确认一下这份文档中的next和rest术语是否颠倒了吗?http://clojure.org/lazy - user1644340
事实证明,如果您想使用函数realised?来检查是否已经实现了惰性序列,则需要将cons放在lazy-seq块内,因为LazySeq对象实现了IPending接口,而clojure.lang.Cons则没有,这也毫无意义。因此,realised?函数期望传入LazySeq参数。 - Simon Polak

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接