这是一段代码,它给了我一个StackOverflowError
(从我的代码库中的实际示例中简化):
( ->> (range 3000)
(mapcat #(concat [0] (take 100 (repeat %))))
(reduce (constantly nil))
(count))
(注意:此代码并非旨在执行除了演示问题或返回零之外的任何内容。)
我可以通过以下任何步骤“拯救”它:
reduce
行[0]
更改为'(0)
(take 100000000)
和mapcat
之间的任意点添加count
(或任何整数)。我对这种行为感到困惑(特别是#2)。我很感激任何意见。
(我觉得这可能与Why does reduce give a StackOverflowError in Clojure?有关,但我不知道如何 - 所以如果它是相关的,我会理解为什么会有这样的解释。)
答案 0 :(得分:10)
在正常情况下,reduce
使用loop
/ recur
构造进行操作,并使用不变的堆栈空间。但是,您已经遇到了一个令人讨厌的角落案例,该案例是通过减少通过提供concat
交替的分块和非分块序列而产生的序列(向量[0]
被分块); seq产生的seq {1}}是非分块的。)
当(take 100 (repeat %))
的第一个参数是一个分块序列时,它将返回一个惰性序列,该序列将使用concat
生成另一个分块序列。否则,它将使用chunk-cons
来生成非分块序列。
同时,cons
的实现使用reduce
协议(在InternalReduce
中定义),该协议为结构提供clojure.core.protocols
函数,可以比使用默认的第一个/下一个递归。分块序列的internal-reduce
实现使用块函数来循环使用分块项,直到它留下非分块序列,然后在余数上调用internal-reduce
。默认的internal-reduce
实现类似地使用internal-reduce
/ first
来使用循环中的项目,直到底层seq类型发生更改,然后在新的seq类型上调用next
以调度到适当的优化版本。当你逐步完成internal-reduce
生成的seq,在chunked和non-chunked子序列之间交替时,concat
调用堆积在堆栈上并最终将其吹掉。
这种情况的简单说明是:
internal-reduce
检查堆栈跟踪:
;; All chunked sub-seqs is OK
user> (reduce + (apply concat (take 10000 (repeat [1]))))
10000
;; All non-chunked sub-seqs is OK
user> (reduce + (apply concat (take 10000 (repeat '(1)))))
10000
;; Interleaved chunked and non-chunked sub-seqs blows the stack
user> (reduce + (apply concat (take 10000 (interleave (repeat [1]) (repeat '(1))))))
StackOverflowError clojure.lang.LazySeq.seq (LazySeq.java:60)
至于你的解决方法:
StackOverflowError
clojure.core/seq (core.clj:133)
clojure.core/interleave/fn--4525 (core.clj:3901)
clojure.lang.LazySeq.sval (LazySeq.java:42)
clojure.lang.LazySeq.seq (LazySeq.java:60)
clojure.lang.RT.seq (RT.java:484)
clojure.core/seq (core.clj:133)
clojure.core/take/fn--4232 (core.clj:2554)
clojure.lang.LazySeq.sval (LazySeq.java:42)
clojure.lang.LazySeq.seq (LazySeq.java:60)
clojure.lang.Cons.next (Cons.java:39)
clojure.lang.RT.next (RT.java:598)
clojure.core/next (core.clj:64)
clojure.core/concat/cat--3925/fn--3926 (core.clj:694)
clojure.lang.LazySeq.sval (LazySeq.java:42)
clojure.lang.LazySeq.seq (LazySeq.java:60)
clojure.lang.ChunkedCons.chunkedNext (ChunkedCons.java:59)
clojure.core/chunk-next (core.clj:660)
clojure.core.protocols/fn--6041 (protocols.clj:101)
clojure.core.protocols/fn--6005/G--6000--6014 (protocols.clj:19)
clojure.core.protocols/fn--6034 (protocols.clj:147)
clojure.core.protocols/fn--6005/G--6000--6014 (protocols.clj:19)
clojure.core.protocols/fn--6041 (protocols.clj:104)
clojure.core.protocols/fn--6005/G--6000--6014 (protocols.clj:19)
clojure.core.protocols/fn--6034 (protocols.clj:147)
clojure.core.protocols/fn--6005/G--6000--6014 (protocols.clj:19)
clojure.core.protocols/fn--6041 (protocols.clj:104)
clojure.core.protocols/fn--6005/G--6000--6014 (protocols.clj:19)
clojure.core.protocols/fn--6034 (protocols.clj:147)
clojure.core.protocols/fn--6005/G--6000--6014 (protocols.clj:19)
clojure.core.protocols/fn--6041 (protocols.clj:104)
完全阻止了这个问题。reduce
更改为[0]
会将分块的子序列替换为非分块的子序列,绕过'(0)
中的分块序列的优化,并允许在单个循环中进行缩减不断的堆栈空间。internal-reduce
会创建一个新的非分块序列,完全由cons单元格组成。答案 1 :(得分:0)
我认为问题出在mapcat
,调用concat
,使用cons
。向量上的cons
是昂贵的(并且可能消耗堆栈),而列表则便宜。这就是为什么从矢量改为列表"修复"问题。