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

时间:2015-04-17 16:14:45

标签: recursion clojure stack-overflow

以下是一个例子:

;; 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))))

它产生StackOverflowError。

3 个答案:

答案 0 :(得分:3)

这里有几个问题。首先,正如另一个答案所指出的,您的marksieve函数没有终止条件。它看起来像是设计用于无限序列,但是如果你通过一个有限长度的序列,它们就会一直走到尽头。

这里更深层次的问题是,看起来你正试图让一个函数通过递归调用自身来创建一个懒惰的无限序列。但是,cons并不以任何方式懒惰;它是纯函数调用,因此立即调用marksieve的递归调用。将sieve中对lazy-seq的最外部呼叫包裹起来仅用于推迟初始呼叫;它不会使整个序列变得懒惰。相反,每次调用cons都必须包含在自己的懒惰序列中。

例如:

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

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

将此与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留给读者练习。

(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)

答案 1 :(得分:2)

需要终止条件

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

但即使你做对了......

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

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

答案 2 :(得分:0)

感谢@ 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