【问题标题】:Why does reduce give a StackOverflowError in Clojure?为什么 reduce 会在 Clojure 中给出 StackOverflowError?
【发布时间】: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))))

但这会导致

StackOverflowError   clojure.lang.RT.seq (RT.java:484)

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

两个问题:

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

【问题讨论】:

    标签: recursion clojure stack-overflow 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)
    StackOverflowError   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))
    StackOverflowError   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 检查 StackOverflowError 的堆栈跟踪,您会看到类似的结果。

    【讨论】:

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

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

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

    不要使用concat,而是使用into

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

    【讨论】:

      猜你喜欢
      • 2023-01-22
      • 2018-07-25
      • 2020-02-24
      • 1970-01-01
      • 1970-01-01
      • 2020-06-05
      • 2012-08-08
      • 1970-01-01
      • 2021-08-10
      相关资源
      最近更新 更多