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

Rus*_*lan 7 recursion clojure

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

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

(defn print-down-from [x]
  (when (pos? x)
    (println x)
    (recur (dec x))
    )
  )
(print-down-from 5)
Run Code Online (Sandbox Code Playgroud)

结果与...相同

(defn print-down-from [x]
  (when (pos? x)
    (println x)
    (print-down-from (dec x))
    )
  )
(print-down-from 5)
Run Code Online (Sandbox Code Playgroud)

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

但我最想知道的是除了堆栈消费之外的任何其他原因.

Cha*_*ffy 13

正如关于函数式编程的clojure.org页面中所解释的那样:

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

当你不使用recur(或trampoline)时,你的函数调用消耗堆栈:因此,如果你运行(print-down-from 100000),你会很快发现崩溃.


那么,提供这种语言设施可以带来以下好处:

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

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