Clojure:简单的阶乘导致堆栈溢出

时间:2009-11-02 16:37:43

标签: recursion clojure stack-overflow

我做错了什么?简单的递归几千次调用深度抛出一个StackOverflowError

如果Clojure递归的限制如此之低,我该如何依赖它?

(defn fact[x]
  (if (<= x 1) 1 (* x  (fact (- x 1))  )))

user=> (fact 2)
2

user=> (fact 4)
24

user=> (fact 4000)
java.lang.StackOverflowError (NO_SOURCE_FILE:0)

9 个答案:

答案 0 :(得分:72)

这是另一种方式:

(defn factorial [n]
  (reduce * (range 1 (inc n))))

这不会导致堆叠,因为range会返回一个懒惰的seq,而reduce会在没有抓住头部的情况下穿过seq。

reduce如果可以的话,会使用chunked seqs,所以这实际上比自己使用recur更好。使用基于Siddhartha Reddy's recur的版本和基于reduce的版本:

user> (time (do (factorial-recur 20000) nil))
"Elapsed time: 2905.910426 msecs"
nil
user> (time (do (factorial-reduce 20000) nil))
"Elapsed time: 2647.277182 msecs"
nil

略有不同。我希望将recur响铃留给mapreduce以及更加可读和明确的朋友,并在内部使用recur比我可能更优雅一点手工做。有时您需要手动recur,但根据我的经验,不需要那么多。

答案 1 :(得分:36)

我理解,堆栈大小取决于您使用的JVM以及平台。如果您使用的是Sun JVM,则可以使用-Xss-XThreadStackSize参数来设置堆栈大小。

在Clojure中进行递归的首选方法是使用loop / recur

(defn fact [x]
    (loop [n x f 1]
        (if (= n 1)
            f
            (recur (dec n) (* f n)))))

Clojure会为此进行尾部调用优化;确保您永远不会遇到StackOverflowError s。

由于defn implies a loop binding,您可以省略loop表达式,并将其参数用作函数参数。要使其成为1参数函数,请使用the multiary caracteristic of functions

(defn fact
  ([n] (fact n 1))
  ([n f]
  (if (<= n 1)
    f
    (recur (dec n) (* f n)))))

编辑:对于记录,这里是一个Clojure函数,它返回所有阶乘的延迟序列:

(defn factorials []
    (letfn [(factorial-seq [n fact]
                           (lazy-seq
                             (cons fact (factorial-seq (inc n) (* (inc n) fact)))))]
      (factorial-seq 1 1)))

(take 5 (factorials)) ; will return (1 2 6 24 120)

答案 2 :(得分:16)

Clojure有几种方法破坏递归

  • 使用 recur 进行显式尾调用。 (它们必须是真正的尾调用,所以这不会起作用)
  • 如上所述的懒惰序列
  • trampolining 返回一个执行工作而不是直接执行的函数,然后调用一个重复调用其结果的trampoline函数,直到它转换为实数而不是函数。
  • (defn fact ([x] (trampoline (fact (dec x) x))) 
               ([x a] (if (<= x 1) a #(fact (dec x) (*' x a)))))
    (fact 42)
    620448401733239439360000N
    

  • memoizing 事实上这可以真正缩短堆栈深度,但通常不适用。

    ps:我没有对我进行复制,所以有人会对陷阱事实函数进行测试吗?

  • 答案 3 :(得分:3)

    当我即将发布以下内容时,我发现它与JasonTrue发布的Scheme示例几乎相同......无论如何,这是Clojure中的一个实现:

    user=> (defn fact[x]
            ((fn [n so_far]
              (if (<= n 1)
                  so_far
                  (recur (dec n) (* so_far n)))) x 1))
    #'user/fact
    user=> (fact 0)
    1
    user=> (fact 1)
    1
    user=> (fact 2)
    2
    user=> (fact 3)
    6
    user=> (fact 4)
    24
    user=> (fact 5)
    120
    

    答案 4 :(得分:1)

    堆栈深度是一个很小的烦恼(但是可配置),但即使是使用像Scheme或F#这样的尾递归的语言,你最终也会用你的代码耗尽堆栈空间。

    据我所知,即使在透明支持尾递归的环境中,您的代码也不太可能进行尾递归优化。您可能希望查看延续传递样式以最小化堆栈深度。

    这是来自Wikipedia的Scheme中的一个规范示例,它可以被翻译成Clojure,F#或其他函数式语言而不会有太多麻烦:

    (define factorial
      (lambda (n)
          (let fact ([i n] [acc 1])
            (if (zero? i)
                acc
                (fact (- i 1) (* acc i))))))
    

    答案 5 :(得分:1)

    正如l0st3d建议的那样,请考虑使用recurlazy-seq

    此外,尝试使用内置序列形式构建它,使其序列变得懒惰,而不是直接执行。

    以下是使用内置序列形式创建惰性Fibonacci序列的示例(来自Programming Clojure一书):

    (defn fibo []
      (map first (iterate (fn [[a b]] [b (+ a b)]) [0 1])))
    
    => (take 5 (fibo))
    (0 1 1 2 3)
    

    答案 6 :(得分:0)

    另一个简单的递归实现很简单:

    (defn fac [x]
        "Returns the factorial of x"
        (if-not (zero? x) (* x (fac (- x 1))) 1))
    

    答案 7 :(得分:0)

    要添加Siddhartha Reddy的答案,您还可以借用Factorial函数表Structure And Interpretation of Computer Programs,并进行一些特定于Clojure的调整。即使是非常大的因子计算,这也给了我很好的表现。

    (defn fac [n]
      ((fn [product counter max-count]
         (if (> counter max-count)
             product
             (recur (apply *' [counter product])
                    (inc counter)
                    max-count)))
       1 1 n))
    

    答案 8 :(得分:-8)

    因子数字本质上非常大。我不确定Clojure如何处理这个问题(但我确实看到它适用于java),但任何不使用大数字的实现都会非常快速地溢出。

    编辑:这没有考虑到你正在使用递归这一事实,这也可能耗尽资源。

    编辑x2:如果实现使用大数字,据我所知,通常是数组,再加上递归(每个函数条目一个大数字副本,由于函数调用总是保存在堆栈上)解释堆栈溢出。尝试在for循环中执行此操作以查看是否存在问题。