Clojure - 使用recur vs普通递归函数调用

时间:2015-12-04 18:06:31

标签: recursion clojure

为什么Clojure有“复发”特殊形式?

当我用函数本身替换“recur”时,我得到相同的结果:

(defn print-down-from [x]
  (when (pos? x)
    (println x)
    (recur (dec x))
    )
  )
(print-down-from 5)

具有相同的结果
(defn print-down-from [x]
  (when (pos? x)
    (println x)
    (print-down-from (dec x))
    )
  )
(print-down-from 5)

我想知道“recur”是否只是一个安全措施,因此如果开发人员碰巧使用非尾递归,编译器将抛出错误。或者编译器可能需要优化尾递归?

但我最想知道的是关于堆叠消费以外的任何其他原因。

2 个答案:

答案 0 :(得分:12)

the clojure.org page on functional programming中所述:

  

在没有可变局部变量的情况下,循环和迭代必须采用与内置for或while结构的语言不同的形式,这些结构由更改状态控制。在函数式语言中,循环和迭代通过递归函数调用替换/实现。 许多此类语言保证在尾部位置进行的函数调用不会占用堆栈空间,因此递归循环使用常量空间。由于Clojure使用Java调用约定,因此它不能也不会产生相同的尾调用优化保证。相反,它提供了recur特殊运算符,它通过重新绑定和跳转到最近的封闭循环或函数帧来执行常量空间递归循环。虽然不像尾部调用优化那样通用,但它允许大多数相同的优雅结构,并且提供了检查对recur的调用只能在尾部位置发生的优势。

当您不使用recur(或trampoline)时,您的函数调用使用堆栈:因此,如果您运行(print-down-from 100000),您很快就会发现崩溃。< / p>

然后,提供这种语言功能可以带来以下好处:

  • 与传统的递归调用相反(如问题的例子中所示),使用recur不会消耗堆栈。
  • 作者知道正在使用TCO(并且不使用堆栈),因为在无法使用TCO的位置使用会导致编译时失败。因此,对于代码的作者及其读者而言,堆栈消耗特征是显而易见的,与仅具有自动TCO的语言(其中需要仔细阅读 - 考虑宏扩展)以确定是否在知道是否已经优化之前,呼叫确实处于尾部位置。
  • 与传统JVM调用约定的兼容性,因此保留了与其他以JVM为中心的语言编写的代码的本机互操作性。

最后,有关背景信息,请参阅有关StackOverflow主题的其他几个问题之一:

答案 1 :(得分:0)

据我所知,loop .. recur使用尾端递归,因此你的程序不会破坏堆栈,而常规递归则不会。 4clojure问题的一些解决方案最终没有使用loop .. recur,因为 - 我采取了有根据的猜测 - 解决方案只能使用直接的递归函数调用来导出,而不是{{ 1}}。

据我所读,在几年前的一些Clojure书中,你应该随意使用loop .. recur

但是,至少从我阅读过的那些书中的讨论以及我在此处收到的答案中我已经收到的答案,我们已经达成了一些共识,首先尝试使用像loop .. recur这样的结构来解决您的问题。如果这是不可能的,那么一定要继续使用map,如果你不认为有可能会破坏你的堆栈,那就是直接的递归调用。