【问题标题】:Clojure: reduce, reductions and infinite listsClojure:减少、减少和无限列表
【发布时间】:2011-03-08 02:00:54
【问题描述】:

Reduce 和归约可让您在序列上累积状态。 序列中的每个元素都会修改累积的状态,直到 到达序列的末尾。

对无限列表调用 reduce 或 reductions 有何影响?

(def c (cycle [0]))
(reduce + c)

这将很快抛出 OutOfMemoryError。顺便说一句,(reduce + (cycle [0])) 不会抛出 OutOfMemoryError(至少在我等待的时候不会)。它永远不会回来。不知道为什么。

有什么方法可以以一种有意义的方式在无限列表上调用 reduce 或 reductions?我在上面的例子中看到的问题是,最终列表的评估部分变得足够大以溢出堆。也许无限列表不是正确的范例。减少生成器、IO 流或事件流会更有意义。在评估并用于修改状态后,不应保留该值。

【问题讨论】:

    标签: clojure


    【解决方案1】:

    它永远不会返回,因为reduce接受一个序列和一个函数并应用该函数,直到输入序列为空,只有这样它才能知道它有最终值。

    减少真正无限的 seq 并没有多大意义,除非它会产生副作用,例如记录其进度。

    在您的第一个示例中,您首先创建一个引用无限序列的 var。

    (def c (cycle [0]))
    

    然后您将 var c 的内容传递给 reduce ,它开始读取元素以更新其状态。

    (reduce + c)
    

    这些元素不能被垃圾回收,因为 var c 持有对其中第一个的引用,而后者又持有对第二个的引用,依此类推。最终它会读取堆中的空间,然后 OOM。

    为了避免在第二个示例中炸毁堆,您没有保留对您已经使用的数据的引用,因此循环返回的 seq 上的项目与它们产生的一样快,并且累积的结果继续得到大。最终它会溢出一个 long 并崩溃(clojure 1.3)或将自己提升为 BigInteger 并增长到所有堆的大小(clojure 1.2)

    (reduce + (cycle [0]))
    

    【讨论】:

    • 谢谢。说得通。在第一种情况下,我可以调用 first c ,它将评估无限列表中的第一个元素,该元素将保留在内存中。如果我第一次调用足够多的时间,无限列表的评估部分将变得太大并且堆将溢出。在第二种情况下,被评估的部分被不断丢弃。顺便说一句,在第二种情况下,堆永远不会溢出,因为零和仍然为零。
    • 零位的优点。想提一下 clojure 1.2 和 1.3 在这方面是不同的,我想它是错误的 :)
    【解决方案2】:

    就目前而言,Arthur 的回答很好,但他似乎没有解决您关于 reductions 的第二个问题。 reductions 返回一个惰性的中间阶段序列,如果给定一个只有 N 个元素长的列表,reduce 返回的内容。所以在无限列表上调用reductions 是完全明智的:

    user=> (take 10 (reductions + (range)))
    (0 1 3 6 10 15 21 28 36 45)
    

    【讨论】:

    • 但是在这里,你最终会溢出堆。 reductions 返回一个惰性列表,但要访问例如第 100 万个元素,您必须评估前一百万个元素,这可能会溢出堆。
    • 其实,拿回去吧。您可以继续调用 next 并保存列表的其余部分,丢弃第一个元素。对惰性序列进行推理是一件棘手的事情。
    • 哦,那是新的东西,谢谢你的加入。与任何懒惰的序列一样,一定要放松你的头脑;)
    【解决方案3】:

    如果您想继续从列表中获取项目(如 IO 流)并在运行之间保持状态,则不能使用 doseq(不求助于 def)。相反,一个好的方法是使用 loop/recur 这将允许您避免消耗过多的堆栈空间并让您保持状态,在您的情况下:

     (loop [c (cycle [0])]
       (if (evaluate-some-condition (first c))
         (do-something-with (first c) (recur (rest c)))
         nil))
    

    当然,与您的情况相比,这里有一个条件检查,以确保我们不会无限期地循环。

    【讨论】:

    • 你可能想通过某种状态来模拟reduce。 (loop [c (cycle [0]) state () (if (evaluate-some-condition (first c)) (recur (rest c) do-something-with (first(c) state)) state))跨度>
    • pyr 的版本不会编译,除非do-something-with 是一个扩展为recur 在尾部位置实际上 的形式的宏。
    【解决方案4】:

    正如其他人指出的那样,直接在无限序列上运行 reduce 是没有意义的,因为 reduce 是非惰性的,需要消耗整个序列。

    作为这种情况的替代方案,这里有一个有用的函数,它只减少序列中的前 n 项,使用 recur 实现以合理的效率:

     (defn counted-reduce 
      ([n f s] 
        (counted-reduce (dec n) f (first s) (rest s) ))
      ([n f initial s]
        (if (<= n 0)
          initial
          (recur (dec n) f (f initial (first s)) (rest s)))))
    
    (counted-reduce 10000000 + (range))
    => 49999995000000
    

    【讨论】:

    • 或者你可以使用 (nth (reductions + (range)) 9999999)
    • 非常正确。虽然 counted-reduce 执行得更快一些(在我的机器上大约 30% 用于 biggish n)。不完全确定为什么......但可能是因为减少产生的惰性分块序列的额外开销?
    猜你喜欢
    • 2020-03-25
    • 2018-10-29
    • 1970-01-01
    • 1970-01-01
    • 2016-03-26
    • 2011-03-10
    • 2019-07-31
    • 1970-01-01
    • 2012-12-21
    相关资源
    最近更新 更多