Clojure thunks:堆栈溢出,带[0],但不带';(0)?

Clojure thunks:堆栈溢出,带[0],但不带';(0)?,clojure,stack-overflow,lazy-evaluation,thunk,Clojure,Stack Overflow,Lazy Evaluation,Thunk,下面是一段代码,它给了我一个StackOverflowError(从我的代码库中的一个实际示例中总结出来): (注意:此代码的设计目的只是演示问题或返回零。) 我可以通过以下任一步骤“拯救”它: 删除reduce行 将[0]更改为'(0) 在mapcat和计数之间的任意点添加(取100000000)(或任意整数) 我基本上对这种行为感到困惑(尤其是#2)。如果您有任何意见,我将不胜感激 (我有一种感觉,这可能与此有关,但我不太清楚是怎么回事——因此,如果它与此有关,我希望能解释一下原因。)我认为

下面是一段代码,它给了我一个
StackOverflowError
(从我的代码库中的一个实际示例中总结出来):

(注意:此代码的设计目的只是演示问题或返回零。)

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

  • 删除
    reduce
  • [0]
    更改为
    '(0)
  • mapcat
    计数之间的任意点添加
    (取100000000)
    (或任意整数)
  • 我基本上对这种行为感到困惑(尤其是#2)。如果您有任何意见,我将不胜感激


    (我有一种感觉,这可能与此有关,但我不太清楚是怎么回事——因此,如果它与此有关,我希望能解释一下原因。)

    我认为问题在于
    mapcat
    ,它调用
    concat
    ,使用
    cons
    <矢量上的code>cons
    成本很高(可能会消耗堆栈),而列表的成本很低。这就是为什么从矢量更改为列表“修复”了问题。

    在正常情况下,
    reduce
    使用
    循环
    /
    重复
    构造并使用恒定的堆栈空间。然而,您遇到了一个棘手的问题,这是由于减少了一个序列,该序列是通过提供
    concat
    交替分块和非分块序列(向量
    [0]
    是分块的;由
    生成的seq(取100(重复%)
    是非分块的)而生成的

    concat
    的第一个参数是分块序列时,它将返回一个惰性序列,该序列将使用
    chunk cons
    生成另一个分块序列。否则,它将使用
    cons
    生成非分块序列

    同时,
    reduce
    的实现使用了
    InternalReduce
    协议(在
    clojure.core.protocols
    中定义),该协议为结构提供了一个
    内部reduce
    函数,与默认的第一次/下一次递归相比,该函数可以更有效地简化结构本身。分块序列的
    internal reduce
    实现使用分块函数在循环中消费分块项,直到剩下一个非分块序列,然后调用剩余的
    internal reduce
    。默认的
    internal reduce
    实现类似地使用
    first
    /
    next
    消耗循环中的项目,直到基础seq类型发生更改,然后调用新seq类型上的
    internal reduce
    ,以分派到适当的优化版本。当您在
    concat
    生成的seq中前进时,在分块子序列和非分块子序列之间交替进行,
    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)
      将分块子序列替换为非分块子序列,从而避免了
      内部reduce
      中对分块序列的优化,并允许在具有恒定堆栈空间的单个循环中进行缩减
    • 插入
      take
      创建一个新的非分块序列,该序列完全由cons单元格组成

    它是否应该返回0?有趣的是,如果我将其更改为以下值,我不会得到堆栈溢出:(>>(范围)(mapcat(fn[p](concat[0](取100(重复零);))(取3000)(减少(持续零))(计数))是的,它“应该”返回0。(我的意思是,除了以我能找到的最短方式演示这种行为之外,代码实际上不应该做任何事情。)
    count
    只是在那里演示使用序列本身不会导致错误。在您的示例中,@Firth,如果愿意,您可以将(取3000)更改为(取3000000),并且仍然有效。您还可以将(take 3000)放在reduce步骤之后…reduce是严格的,并且使用恒定的堆栈空间-更有可能是concat/mapcat调用在强制之前未计算堆栈导致issue@noisesmith不过,这并不是一个真正的解释。当我删除
    reduce
    时,
    count
    为什么成功?为什么“(0)的行为会有所不同?为什么包装concat(或mapcat)不会产生(doall)帮助?您能否澄清为什么失败只发生在
    reduce
    步骤中?如果我删除
    reduce
    ,为什么
    count
    仍然有效(这会毫无困难地消耗整个序列)是的,就是这样。
    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)