如何在Clojure中实现延迟序列?

时间:2010-07-14 14:20:11

标签: clojure lisp lazy-evaluation lazy-sequences

我喜欢Clojure。困扰我的一个问题是,我不知道如何实现懒惰的序列,或者它们是如何工作的。

我知道懒惰序列只评估序列中要求的项目。它是如何做到的?

  • 什么使得懒惰序列如此高效以至于它们不会消耗太多 堆栈?
  • 为什么你可以用懒惰的顺序包装递归调用而不是 对于大型计算,更长时间获得堆栈流量?
  • 延迟序列消耗什么资源来完成它的工作?
  • 懒惰序列在什么情况下效率低下?
  • 什么情况下懒惰序列最有效?

3 个答案:

答案 0 :(得分:32)

我们这样做。

我知道懒惰序列只评估序列中要求的项目,它是如何做到的?

懒惰序列(以下称LS,因为我是LP,或懒人)是由部分组成的。对于已经评估过的序列,头部或部分(s,实际上是32个元素一次被评估,如Clojure 1.1,我认为1.2)之后是一个名为 thunk的东西,它基本上是一大块信息(将其视为您的函数的其余部分,创建序列,未评估)等待被称为。当它被调用时,thunk会对它进行评估,然后会根据需要创建一个新的thunk(已经调用了多少内容,因此它可以从之前的位置恢复)。

所以你(take 10 (whole-numbers)) - 假设whole-numbers是一个整数的懒惰序列。这意味着你要对thunk进行10次评估(尽管内部这可能会有所不同,具体取决于优化。

什么使延迟序列如此高效以至于不会消耗太多堆栈?

一旦你阅读了之前的答案(我希望),这一点就会变得更加清晰:除非你特别要求某些东西,否则不会评估任何东西。当你调用某个东西时,可以单独评估序列的每个元素,然后丢弃。

如果序列不是懒惰的,那么它通常会保持在头部,这会消耗堆空间。如果它是惰性的,则计算,然后丢弃,因为后续计算不需要它。

为什么你可以在延迟序列中包装递归调用,而不再为大型计算获得堆栈溢出?

请参阅上一个答案并考虑:lazy-seq宏(from the documentation

will invoke the body only the first time seq
is called, and will cache the result and return it on all subsequent
seq calls.

查看使用递归的酷LS的filter函数:

(defn filter
  "Returns a lazy sequence of the items in coll for which
  (pred item) returns true. pred must be free of side-effects."
  [pred coll]
  (let [step (fn [p c]
                 (when-let [s (seq c)]
                   (if (p (first s))
                     (cons (first s) (filter p (rest s)))
                     (recur p (rest s)))))]
    (lazy-seq (step pred coll))))

延迟序列消耗什么资源来执行它的工作?

我不太确定你在这里问的是什么。 LS需要内存和CPU周期。他们只是不会继续敲打堆栈,并用获取序列元素所需的计算结果填充它。

在哪些情况下,懒惰序列效率低下?

当你使用快速计算并且不会被大量使用的小seq时,使它成为LS是低效的,因为它需要另外几个chars来创建。

严肃地说,除非你试图让非常表现出色,否则LS就可以了。

在哪些情况下,懒惰序列最有效?

当你处理巨大的seqs并且你只是使用它们的零碎时,那就是你从使用它们中获得最大收益的时候。

实际上,在方便性,易于理解(一旦掌握它们)以及推理代码和速度方面,使用LS而不是非LS更好总是更好。

答案 1 :(得分:16)

  

我知道懒惰序列只评估序列中要求的项目,它是如何做到的?

我认为以前发布的答案已经很好地解释了这一部分。我只会补充说,一个懒惰序列的“强制”是一个隐含的 - 无法释放! :-) - 函数调用;也许这种思考方式会让事情变得更加清晰。还要注意,强制一个懒惰的序列涉及一个隐藏的变异 - 强制thunk需要产生一个值,将它存储在缓存中(变异!)并扔掉它的可执行代码,这将不再需要(再次变异!)

  

我知道懒惰序列只评估序列中要求的项目,它是如何做到的?

     

什么使得懒惰序列如此高效以至于它们不会消耗太多堆栈?

     

延迟序列消耗什么资源来完成它的工作?

它们不使用堆栈,因为它们会消耗堆。延迟序列是一种生活在堆上的数据结构,它包含一小部分可执行代码,如果需要,可以调用它来生成更多的数据结构。

  

为什么你可以在延迟序列中包装递归调用,并且不再为大型计算获得堆栈溢出?

首先,正如dbyrne所提到的,如果thunk本身需要使用非常深层嵌套的调用结构执行代码,那么在使用延迟序列时,你很可能得到一个SO。

然而,从某种意义上说,你可以使用懒惰的seq代替尾递归,并且在某种程度上这对你起作用,你可以说它们有助于避免SO。事实上,相当重要的是,产生延迟序列的函数不应该是尾递归的;具有惰性seq生成器的堆栈空间的保存来自上述堆栈 - >堆传输以及以尾递归方式编写它们的任何尝试都只会破坏事物。

关键的洞察力是懒惰序列是一个对象,在第一次创建时,它不包含任何项目(严格的序列始终如此);当一个函数返回一个惰性序列时,在发生任何强制之前,只有这个“惰性序列对象”被返回给调用者。因此,在发生任何强制之前,弹出返回延迟序列的调用所用的堆栈帧。我们来看一个示例生成器函数:

(defn foo-producer [] ; not tail recursive...
  (lazy-seq
    (cons :foo        ; because it returns the value of the cons call...
           (foo-producer)))) ; which wraps a non-tail self-call

这可行,因为lazy-seq会立即返回 ,因此(cons :foo (foo-producer))也会立即返回,并且会立即弹出外部调用foo-producer所用的堆栈帧。对foo-producer的内部调用隐藏在序列的rest部分,这是一个thunk;如果/当该thunk被强制时,它将在堆栈上短暂地使用它自己的帧,但是然后如上所述立即返回等。

Chunking(由dbyrne提到)稍微改变了这张图片,因为在每一步产生了更多的元素,但原理保持不变:当懒惰序列的相应元素正在使用时,每个步骤都用完了一些堆栈生成,然后在
更多强制发生之前,回收该堆栈。

  

在什么情况下懒惰序列效率低下?

     

在什么情况下懒惰序列最有效?

如果你需要立刻抓住整个东西,那么懒惰就没有意义了。一个懒惰的序列在每个步骤进行堆分配时,不分块或每个块 - 每32个步骤一次 - 分块时;在某些情况下,避免这会使你获得性能提升。

但是,延迟序列启用了流水线模式的数据处理:

(->> (lazy-seq-producer)               ; possibly (->> (range)
     (a-lazy-seq-transformer-function) ;               (filter even?)
     (another-transformer-function))   ;               (map inc))

以严格的方式执行此操作无论如何都会分配大量的堆,因为您必须保留中间结果以将它们传递到下一个处理阶段。而且,你需要保持整个事物,这在(range)的情况下实际上是不可能的 - 一个无限的序列! - 当可能时,通常效率低下。

答案 2 :(得分:8)

最初,Clojure中的懒惰序列在需要时逐项评估。在Clojure 1.1中添加了分块序列以提高性能。而不是逐项评估,一次评估32个元素的“块”。这减少了懒惰评估产生的开销。此外,它允许clojure利用底层数据结构。例如,PersistentVector实现为32个元素数组的树。这意味着要访问元素,必须遍历树,直到找到相应的数组。使用分块序列,一次抓取整个阵列。这意味着在需要重新遍历树之前,可以检索32个元素中的每一个。

一直在讨论提供一种在需要完全懒惰的情况下强制逐项评估的方法。但是,我认为它还没有添加到该语言中。

  

为什么你可以在延迟序列中包装递归调用,并且不再为大型计算获得堆栈溢出?

你有一个你的意思的例子吗?如果你有一个递归绑定到lazy-seq,it can definitely cause a stack overflow