Clojure thunk:堆栈溢出[0]但不是'(0)?

时间:2014-10-16 05:15:40

标签: clojure stack-overflow lazy-evaluation thunk

这是一段代码,它给了我一个StackOverflowError(从我的代码库中的实际示例中简化):

( ->> (range 3000)
      (mapcat #(concat [0] (take 100 (repeat %))))
      (reduce (constantly nil))
      (count))

(注意:此代码并非旨在执行除了演示问题或返回零之外的任何内容。)

我可以通过以下任何步骤“拯救”它:

  1. 删除reduce
  2. [0]更改为'(0)
  3. (take 100000000)mapcat之间的任意点添加count(或任何整数)。
  4. 我对这种行为感到困惑(特别是#2)。我很感激任何意见。

    (我觉得这可能与Why does reduce give a StackOverflowError in Clojure?有关,但我不知道如何 - 所以如果它是相关的,我会理解为什么会有这样的解释。)

2 个答案:

答案 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是昂贵的(并且可能消耗堆栈),而列表则便宜。这就是为什么从矢量改为列表"修复"问题。