【问题标题】:Clojure - StackOverflowError while iterating over lazy collectionClojure - 迭代惰性集合时出现 StackOverflowError
【发布时间】:2019-04-14 20:50:08
【问题描述】:

我目前正在 Clojure 中为 Project Euler 问题之一实施解决方案,即埃拉托色尼筛 (https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes)。这是我的代码:

(defn cross-first-element [coll]
  (filter #(not (zero? (rem % (first coll)))) coll))

(println
  (last
  (map first
    (take-while
      (fn [[primes sieve]] (not (empty? sieve)))
      (iterate
        (fn [[primes sieve]] [(conj primes (first sieve)) (cross-first-element sieve)])
        [[] (range 2 2000001)])))))

基本思想是有两个集合 - 已经从筛子中检索到的素数,以及剩余的筛子本身。我们从空的primes 开始,直到筛子为空,我们选择它的第一个元素并将其附加到primes,然后我们从筛子中划掉它的倍数。当它用尽时,我们知道我们所有的质数都在 200 万以下。

不幸的是,尽管它适用于筛子的小上限(比如 1000),但它会导致 java.lang.StackOverflowError 具有长堆栈跟踪,重复序列为:

...
clojure.lang.RT.seq (RT.java:531)
clojure.core$seq__5387.invokeStatic (core.clj:137)
clojure.core$filter$fn__5878.invoke (core.clj:2809)
clojure.lang.LazySeq.sval (LazySeq.java:42)
clojure.lang.LazySeq.seq (LazySeq.java:51)
...

我的解决方案中的概念错误在哪里?如何解决?

【问题讨论】:

标签: clojure stack-overflow lazy-evaluation


【解决方案1】:

原因如下:由于cross-first-element 中的filter 函数是惰性的,它实际上并没有在每个iterate 步骤上过滤您的集合,而是“堆叠”过滤函数调用。这导致了当你真正需要结果元素时,整个测试函数的负载都会被执行,大致如下:

(#(not (zero? (rem % (first coll1))))
  (#(not (zero? (rem % (first coll2))))
    (#(not (zero? (rem % (first coll3))))
       ;; and 2000000 more calls

导致堆栈溢出。

在您的情况下,最简单的解决方案是使过滤变得急切。你可以通过简单地使用filterv而不是filter来做到这一点,或者将其包装成(doall (filter ...

但是您的解决方案仍然很慢。我宁愿为此使用循环和本机数组。

【讨论】:

    【解决方案2】:

    您已经(重新)发现嵌套惰性序列有时可能会出现问题。 Here is one example 可能出错的地方(不直观)。

    如果您不介意使用库,那么问题会简单得多,只需在命令式循环周围使用一个惰性包装器即可。这就是lazy-genyield 给你的(Python 中的“生成器”):

    (ns tst.demo.core
      (:use demo.core tupelo.test)
      (:require [tupelo.core :as t]))
    
    (defn unprime? [primes-so-far candidate]
      (t/has-some? #(zero? (rem candidate %)) primes-so-far))
    
    (defn primes-generator []
      (let [primes-so-far (atom [2])]
        (t/lazy-gen
          (t/yield 2)
          (doseq [candidate (drop 3 (range))] ; 3..inf
            (when-not (unprime? @primes-so-far candidate)
              (t/yield candidate)
              (swap! primes-so-far conj candidate))))))
    
    (def primes (primes-generator))
    
    (dotest
      (is= (take 33 primes)
        [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 ])
    
      ; first prime over 10,000
      (is= 10007 (first (drop-while #(< % 10000) primes)))
    
      ; the 10,000'th prime (https://primes.utm.edu/lists/small/10000.txt)
      (is= 104729 (nth primes 9999)) ; about 12 sec to compute
    )
    

    我们也可以使用loop/recur 来控制循环,但是使用atom 来保持状态更容易阅读。


    除非你真的非常需要一个惰性且无限的解决方案,否则命令式解决方案要简单得多:

    (defn primes-upto [limit]
      (let [primes-so-far (atom [2])]
        (doseq [candidate (t/thru 3 limit)]
          (when-not (unprime? @primes-so-far candidate)
            (swap! primes-so-far conj candidate)))
        @primes-so-far))
    
    (dotest
      (is= (primes-upto 100)
        [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]) )
    

    【讨论】:

      猜你喜欢
      • 2015-12-17
      • 2020-06-20
      • 2012-09-25
      • 2016-09-19
      • 1970-01-01
      • 1970-01-01
      • 2011-01-16
      • 2017-09-08
      • 1970-01-01
      相关资源
      最近更新 更多