繁体   English   中英

Clojure - 迭代延迟集合时的StackOverflowError

[英]Clojure - StackOverflowError while iterating over lazy collection

我目前正在Clojure中实施Project Euler问题的解决方案,即Eratosthenes的Sieve( 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 ,然后我们从筛子中划出它的倍数。 当它耗尽时,我们知道我们拥有素数中低于两百万的所有素数。

不幸的是,尽管它适用于筛子的小上限(比如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)
...

我的解决方案中的概念错误在哪里? 怎么解决?

原因如下:由于cross-first-elementfilter函数是惰性的,它实际上并不会在每个iterate步骤中过滤您的集合,而是“堆栈”过滤函数调用。 这导致了这样一种情况:当你真正需要生成的元素时,将执行整个测试函数的负载,大致如下:

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

导致堆栈溢出。

在您的情况下,最简单的解决方案是过滤渴望。 你可以通过简单地使用filterv而不是filter来实现它,或者将它包装成(doall (filter ...

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

您已经(重新)发现嵌套的延迟序列有时会出现问题。 这是一个可能出错的例子 (它不直观)。

如果您不介意使用库,那么在命令式循环周围使用单个惰性包装器会更加简单。 这就是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]) )

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM