为啥 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
大量 concat
s 因 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 通常会在seq
ed 时展开嵌套的 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