为啥 reduce 会在 Clojure 中给出 ***Error?

Posted

技术标签:

【中文标题】为啥 reduce 会在 Clojure 中给出 ***Error?【英文标题】:Why does reduce give a ***Error in Clojure?为什么 reduce 会在 Clojure 中给出 ***Error? 【发布时间】:2014-09-17 11:53:54 【问题描述】:

我正在尝试连接一个 Seq of Seq。

我可以用apply concat 做到这一点。

user=> (count (apply concat (repeat 3000 (repeat 3000 true))))
9000000

但是,根据我有限的知识,我会假设使用 apply 会强制实现惰性 Seq,这对于非常大的输入似乎并不合适。如果可以,我宁愿偷懒。

所以我认为使用reduce 可以完成这项工作。

user=> (count (reduce concat (repeat 3000 (repeat 3000 true))))

但这会导致

***Error   clojure.lang.RT.seq (RT.java:484)

我很惊讶,因为我会认为 reduce 的语义意味着它是尾调用递归的。

两个问题:

apply 是最好的方法吗? reduce 通常不适合大型输入吗?

【问题讨论】:

【参考方案1】:

使用apply。当函数参数是惰性时,apply 也是。

让我们检查一下对底层子序列的计数副作用:

(def counter (atom 0))

(def ss (repeatedly 3000 
          (fn [] (repeatedly 3000 
            (fn [] (do (swap! counter inc) true))))))


(def foo (apply concat ss))

so.core=> @counter
0

so.core=> (dorun (take 1 foo))
nil

so.core=> @counter
1

so.core=> (dorun (take 3001 foo))
nil

so.core=> @counter
3001

reduce 大量 concats 因 thunk 组合而溢出

惰性序列,例如concat 产生的序列,是通过 thunk、延迟函数调用实现的。当您 concat concat 的结果时,您已将一个 thunk 嵌套在另一个 thunk 中。在您的函数中,嵌套深度为 3000,因此一旦请求第一项,堆栈就会溢出,并且 3000 个嵌套的 thunk 被解除。

so.core=>  (def bar (reduce concat (repeat 3000 (repeat 3000 true))))
#'so.core/bar

so.core=> (first bar)
***Error   clojure.lang.LazySeq.seq (LazySeq.java:49)

implementation of lazy-sequences 通常会在seqed 时展开嵌套的 thunks trampoline 样式,而不是破坏堆栈:

so.core=> (loop [lz [1], n 0] 
            (if (< n 3000) (recur (lazy-seq lz) (inc n)) lz))
(1)

但是,如果您在实现它的同时在未实现部分的惰性序列中调用seq...

so.core=> (loop [lz [1], n 0] 
            (if (< n 3000) (recur (lazy-seq (seq lz)) (inc n)) lz))
***Error   so.core/eval1405/fn--1406 (form-init584039696026177116.clj:1)

so.core=> (pst 3000)
堆栈溢出错误 so.core/eval1619/fn--1620 (form-init584039696026177116.clj:2) clojure.lang.LazySeq.sval (LazySeq.java:40) clojure.lang.LazySeq.seq (LazySeq.java:49) clojure.lang.RT.seq (RT.java:484) clojure.core/seq (core.clj:133) so.core/eval1619/fn--1620 (form-init584039696026177116.clj:2) clojure.lang.LazySeq.sval (LazySeq.java:40) clojure.lang.LazySeq.seq (LazySeq.java:49) clojure.lang.RT.seq (RT.java:484) clojure.core/seq (core.clj:133) ...(反复)

然后你最终构建了seq 堆栈帧。 concat 的实现就是这样。使用 concat 检查 ***Error 的堆栈跟踪,您会看到类似的结果。

【讨论】:

很棒的解释。我在 SO 上发现了这个问题的许多骗局,这是迄今为止最明确的解释,带有非常具体的示例代码。干得好!【参考方案2】:

我可以建议一种方法来避免这个问题。 reduce 函数不是这里的问题; concat 是。

看看: https://stuartsierra.com/2015/04/26/clojure-donts-concat

不要使用concat,而是使用into

(count (reduce into (repeat 3000 (repeat 3000 true))))
9000000

【讨论】:

以上是关于为啥 reduce 会在 Clojure 中给出 ***Error?的主要内容,如果未能解决你的问题,请参考以下文章

Haskell 的 foldr/l 和 Clojure 的 reduce

clojure 减少不终止减少功能

Clojure:减少与应用

为啥这个程序会在 C 中给出 Invalid memory access 错误? [关闭]

let 内的 Clojure 循环(全局 v 局部变量)

cmake install() 行为?如果给出此指令,为啥二进制会在 PWD 中查找