为什么此Clojure素数生成器会引发StackOverflowError? [重复]

问题描述 投票:3回答:2

我只是在学习Clojure,并且像往常一样在学习新的编程语言时,我尝试做的第一件事就是实现Eratosthenes筛子。

我想出了以下解决方案:

(defn primes
 "Calculate all primes up to the given number"
 [n]
 (loop
   [
    result []
    numbers (range 2 (inc n))
    ]
   (if (empty? numbers)
     result
     (let [[next & rest] numbers]
       (recur (conj result next) (filter (fn [n] (not= 0 (mod n next))) rest)))
     )
   )
 )

对于较小的数字,它工作正常且相当快,但对于较大的输入,则会引发StackOverflowError并带有可疑的短堆栈跟踪,例如:

(primes 100000)
Execution error (StackOverflowError) at (REPL:1).
null
(pst)
StackOverflowError 
    clojure.lang.LazySeq.sval (LazySeq.java:42)
    clojure.lang.LazySeq.seq (LazySeq.java:51)    
    clojure.lang.RT.seq (RT.java:531)
    clojure.core/seq--5387 (core.clj:137)    
    clojure.core/filter/fn--5878 (core.clj:2809)
    clojure.lang.LazySeq.sval (LazySeq.java:42)
    clojure.lang.LazySeq.seq (LazySeq.java:51)
    clojure.lang.RT.seq (RT.java:531)
    clojure.core/seq--5387 (core.clj:137)
    clojure.core/filter/fn--5878 (core.clj:2809)
    clojure.lang.LazySeq.sval (LazySeq.java:42)
    clojure.lang.LazySeq.seq (LazySeq.java:51)
=> nil

我给人的印象是,如果[最后以loop形式求值,那么我将执行尾递归,而我的第一个问题是这里是否确实如此。我的第二个问题是为什么堆栈跟踪对于StackOverflowError如此短。我也有解释堆栈跟踪的问题,即。哪条线对应什么形式。

[我只对更好或更像Clojure的解决方案感兴趣,只要它们能为这些问题提供真知灼见,否则我想自己找到它们。谢谢!
clojure stack-overflow primes tail-recursion
2个回答
2
投票
这里是有效的版本:

(ns tst.demo.core (:use tupelo.core tupelo.test)) (defn primes "Calculate all primes up to the given number" [n] (loop [result [] numbers (range 2 (inc n))] (if (empty? numbers) result (let [[new-prime & candidate-primes] numbers] (recur (conj result new-prime) (filterv (fn [n] (not= 0 (mod n new-prime))) candidate-primes))) ))) (dotest (spyx (primes 99999)) )

结果:

------------------------------- Clojure 1.10.1 Java 13 ------------------------------- Testing tst.demo.core (primes 99999) => [2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211 223 227 229 233 239 241 251 257 263 269 271 277 281 283 293 307 311 313 317 331 337 347 349 353 359 367 373 379 383 389 397 401 409 419 421 431 433 439 443 449 457 461 463 467 479 487 491 499 503 509 521 523 541 547 557 563 569 571 577 587 593 599 601 ...<snip>... 99401 99409 99431 99439 99469 99487 99497 99523 99527 99529 99551 99559 99563 99571 99577 99581 99607 99611 99623 99643 99661 99667 99679 99689 99707 99709 99713 99719 99721 99733 99761 99767 99787 99793 99809 99817 99823 99829 99833 99839 99859 99871 99877 99881 99901 99907 99923 99929 99961 99971 99989 99991]

我对您的变量进行了一些重命名,以使它们更清晰。如果仔细观察,您会发现唯一的实质区别是从懒惰的filter变为渴望的filterv

在此更改之前,它对9999的N有效,但对99999无效。我不确定延迟filter函数的实现,但这显然是问题所在。


这样的奇怪(和不可预测的)问题使我不喜欢Clojure代码中的过度懒惰。看来您已陷入Clojure Don'ts: Concat问题的变体。在这种情况下,您的代码如下:

(filter ... (filter ... (filter ... (filter ... ...<many, many more>... ))))

惰性序列被实现为嵌套函数调用。由于找到素数99991的最后一个循环依赖于找到素数2的第一个调用,因此无法释放较早的延迟序列(及其在堆栈上的嵌套函数调用),最终导致堆栈崩溃。

在我的计算机上,阶乘(N)的简单递归实现在N = 4400附近爆炸。上面找到的9592个素数,因此其具体原因似乎比每个素数1个堆栈帧要复杂一些。

可能是N = 32分块可以发挥作用。


为了避免由于不必要的懒惰而导致的错误,您可能有兴趣将concat替换为glue,并将for替换为forv。您也可以看到full API docs

0
投票
稍作修改,用注释描述每行发生的事情,这是您的功能:

(defn primes "Calculate all primes up to the given number" [n] ;; `loop` is not lazy, it runs until it produces a result: (loop [result [] ;; a lazy sequence implemented by clojure.lang.LongRange: numbers (range 2 (inc n))] (if (not (nil? (seq numbers))) result (let [current (first numbers) remaining (rest numbers)] (recur ;; `conj` on a vector returns a vector (non-lazy): (conj result current) ;; `filter` on a lazy sequence returns a new lazy sequence: (filter (fn [n] (not= 0 (mod n next))) remaining))))))

关键是末尾的filter

[大多数延迟序列操作,例如filter,是通过将一个延迟序列包装在另一个序列中来工作的。在循环的每次迭代中,filter添加另一层惰性序列,如下所示:

(filter (fn [n] (not= 0 (mod n 5))) ; returns a LazySeq (filter (fn [n] (not= 0 (mod n 4))) ; returns a LazySeq (filter (fn [n] (not= 0 (mod n 3))) ; returns a LazySeq (filter (fn [n] (not= 0 (mod n 2))) ; returns a LazySeq remaining))))

LazySeq对象堆积起来,每个对象都具有对前一个对象的引用。

对于大多数惰性序列,包装并不重要,因为一旦您请求一个值,它们就会自动“解包”。发生在LazySeq.seq

在这种情况下,[[does很重要,这是一种情况,因为您的循环建立了如此多的惰性序列对象层,以致LazySeq.seq.sval的嵌套调用超出了JVM允许的最大堆栈大小。这就是您在堆栈跟踪中看到的内容。

此功能的更普遍的问题是混合惰性(filter)和非惰性(loop)操作。这通常是问题的根源,因此Clojure程序员学会了避免这种习惯。

如艾伦所言,您可以通过仅使用非延迟操作(例如filterv而不是filter来避免此问题,该操作将延迟序列转换为向量。

几乎任何形式的惰性评估都可以显示此问题的某些变化。我在Clojure don'ts: concat中描述了它。有关另一个示例,请参见Haskell中的foldr versus foldl

即使没有惰性,深层嵌套的对象树也可能导致StackOverflow,例如在Java中,我发现了xstream#88circe#1074

© www.soinside.com 2019 - 2024. All rights reserved.