为什么递归调用不会被recur自动替换?

时间:2011-09-29 15:37:26

标签: recursion clojure

在下面(Clojure)SO问题:my own interpose function as an exercise

接受的答案说明了这一点:

  

通过调用recur来替换你的递归调用,因为它是写的   会遇到堆栈溢出

(defn foo [stuff]
  (dostuff ... )
  (foo (rest stuff)))
  

变为:

(defn foo [stuff]
  (dostuff ...)
  (recur (rest stuff)))
  

避免吹嘘。

这可能是一个愚蠢的问题,但我想知道为什么 foo 的递归调用不会被 recur 自动替换?

另外,我拿了另一个SO示例并写了这个(没有故意使用 cond ,只是为了尝试一下):

(defn is-member [elem ilist]
  (if (empty? ilist)
    false
    (if (= elem (first ilist))
      true
      (is-member elem (rest ilist)))))

我想知道是否应该用 recur 替换对 is-member 的调用(这似乎也有效)。

是否存在递归的情况,特别是不应使用 recur

3 个答案:

答案 0 :(得分:8)

如果你有一个尾递归方法,那么几乎没有理由使用recur,尽管除非你处于对性能敏感的代码区域,否则它只会赢得'没有任何区别。

我认为基本论点是recur是显式的,可以非常清楚函数是否是尾递归的;所有尾递归函数都使用recur,并且recur都是尾递归的所有函数(如果你尝试从非尾部位置使用recur,编译器会抱怨。)所以这是一个美学决定。

recur还有助于区分Clojure与将在所有尾调用上执行TCO的语言,如Scheme; Clojure无法有效地执行此操作,因为它的函数被编译为Java函数,而JVM不支持它。通过使recur成为一个特例,希望没有夸大的期望。

我认为编译器无法为您插入recur有任何技术原因,如果它是以这种方式设计的,但也许有人会纠正我。

答案 1 :(得分:7)

我问Rich Hickey他和他的推理基本上(我解释)

“让特殊情况看起来很特别”

他没有看到在大多数情况下特定情况的文章中留下的价值 想知道为什么如果在某些事情发生变化时打击堆栈并且编译器无法保证优化。不合时宜的只是尝试保持语言简单的设计决策之一

答案 2 :(得分:3)

  

我想知道是否应该用recur替换对is-member的调用

一般情况下,正如mquander所说,没有理由不尽可能不使用复发。对于小输入(几十到几百个元素),它们是相同的,但没有重复的版本会在大输入(几千个元素)上爆炸。

显式递归(即没有'recur')对许多事情都有好处,但迭代长序列不是其中之一。

  

是否有特别不应该使用复发的情况?

只有当你不能使用它时,这是

  • 它不是尾递归 - 即它想要使用返回值做一些事情。
  • 递归是一个不同的功能。

一些例子:

 (defn foo [coll] 
  (when coll 
    (println (first coll))
    (recur (next coll)))   ;; OK: Tail recursive

(defn fie [coll]
  (when coll
    (cons (first coll)       
      (fie (next coll))))) ;; Can't use recur: Not tail recursive.   

(defn fum 
  ([coll] 
    (fum coll []))         ;; Can't use recur: Different function.
  ([coll acc] 
    (if (empty? coll) acc
       (recur (next coll)  ;; OK: Tail recursive
          (conj acc (first coll)))))) 

至于为什么在适当的时候没有自动插入recur:我不知道,但至少有一个积极的副作用是使实际的函数调用在视觉上与非调用(即recur)不同。

由于这可能是“作品”和“与StackOverflowError一起爆炸”之间的区别,我认为这是一个公平的设计选择,使其明确 - 在代码中可见 - 而不是隐含的,你必须从第二个开始 - 猜测编译器何时无法按预期工作。