如何避免clojure递归函数中的stackoverflow?

How to avoid stackoverflow in clojure recursive function?

这是一个例子:

;; Helper function for marking multiples of a number as 0
(def mark (fn [[x & xs] k m]
                 (if (= k m) 
                   (cons 0 (mark xs 1       m))
                   (cons x (mark xs (inc k) m))
                   )))

;; Sieve of Eratosthenes
(defn sieve
  [x & xs]
  (if (= x 0)
    (sieve xs)
    (cons x (sieve (mark xs 1 x)))
    ))

(take 10 (lazy-seq (sieve (iterate inc 2))))

它产生了 WhosebugError。

需要终止条件

这里的问题是您的 marksieve 函数都没有终止条件。必须有一些输入集,每个函数不会为其调用自身,但 return 是一个答案。此外,这些函数的每组(有效)输入最终都应解析为非递归 return 值。

但即使你做对了...

我要补充一点,即使你成功地创建了正确的终止条件,如果递归的深度太大,仍然有可能导致堆栈溢出。这可以通过增加 JVM 堆栈大小在一定程度上缓解,但这有其局限性。

对于某些函数,解决此问题的方法是使用尾调用优化。一些递归函数是 tail recursive,这意味着在其定义中定义的函数的所有递归调用都在 tail call position(是最终的在定义主体中调用的函数)。例如,在 sieve 函数的 (= x 0) 情况下,sieve 是尾调用,因为 sieve 的结果不会传递给任何其他函数。但是,在 (not (= x 0)) 的情况下,调用 sieve 的结果会传递给 cons,因此这 不是 尾调用。当函数完全尾递归时,可以在幕后将函数定义转换为循环结构,从而避免消耗堆栈。在 clojure 中,这可以通过在函数定义中使用 recur 而不是函数名称来实现(还有一个 loop 结构有时会有帮助)。同样,因为并非所有递归函数都是尾递归的,所以这不是万灵药。但是当他们知道你可以做到这一点时,我会很高兴。

这里有几个问题。首先,正如另一个答案所指出的,您的 marksieve 函数没有终止条件。看起来它们是为处理无限序列而设计的,但如果你传递一个有限长度的序列,它们就会一直跑到最后。

这里更深层次的问题是,您似乎试图让一个函数通过递归调用自身来创建一个惰性无限序列。但是,cons 一点也不懒惰;这是一个纯函数调用,因此对 marksieve 的递归调用会立即被调用。在 lazy-seq 中包装对 sieve 的最外层调用仅用于延迟初始调用;它不会使整个序列变得懒惰。相反,对 cons 的每次调用都必须包装在其自己的惰性序列中。

例如:

(defn eager-iterate [f x]
  (cons x (eager-iterate f (f x))))

(take 3 (eager-iterate inc 0)) ; => WhosebugError
(take 3 (lazy-seq (eager-iterate inc 0))) ; => Still a WhosebugError

将此与 iterate 的实际源代码进行比较:

(defn iterate
  "Returns a lazy sequence of x, (f x), (f (f x)) etc. f must be free of side-effects"
  {:added "1.0"
   :static true}
  [f x] (cons x (lazy-seq (iterate f (f x)))))

将它们放在一起,这里是 mark 的一个实现,它可以正确地处理有限序列并保留无限序列的惰性。修复 sieve 留作 reader.

的练习
(defn mark [[x :as xs] k m]
  (lazy-seq
    (when (seq xs)
      (if (= k m)
        (cons 0 (mark (next xs) 1 m))
        (cons x (mark (next xs) (inc k) m))))))

(mark (range 4 14) 1 3)
; => (4 5 0 7 8 0 10 11 0 13)
(take 10 (mark (iterate inc 4) 1 3))
; => (4 5 0 7 8 0 10 11 0 13)

感谢@Alex 的回答,我设法想出了一个可行的懒惰解决方案:

;; Helper function for marking mutiples of a number as 0
(defn mark [[x :as xs] k m]
  (lazy-seq
    (when-not (empty? xs)
      (if (= k m)
        (cons 0 (mark (rest xs) 1 m))
        (cons x (mark (rest xs) (inc k) m))))))


;; Sieve of Eratosthenes
(defn sieve
  [[x :as xs]]
  (lazy-seq
    (when-not (empty? xs)
      (if (= x 0)
        (sieve (rest xs))
        (cons x (sieve (mark (rest xs) 1 x)))))))

有人建议我使用 rest 而不是 next