重新出现在正确的尾部位置

recur in the rightful tail poistion

recur 应该在尾部位置调用,我假设它有效地充当非递归准循环。

expr-1 or 2被认为在正确的尾部位置但是expr-3的none直到8在以下模拟块结构?否则,如何推理和识别才能进行试错?

(defn foo [x]
 (if cond-expr-1
  (recur expr-1)
  (recur expr-2)))

(defn bar [x]
 (if cond-expr-2
  (fn-1 (recur expr-3))
  (fn-2 (recur expr-4))))

(defn baz [x]
 (if cond-expr-3
  (if cond-expr-4  
    (recur expr-5)
    (recur expr-6))
  (if cond-expr-5  
    (recur expr-7)
    (recur expr-8))))

expr-3expr-4的情况下,它是一个函数的参数,所以它不在尾部位置。

调用 recur 基本上类似于 gotoreturn 语句,两者都不能在函数参数列表中使用。

因为 if 表达式不是一个函数(它是一个 "special form"),所以没有函数参数列表的问题。


这里是 loop/recur 和使用 while 的更命令式方法之间的比较:

(ns tst.demo.core
  (:use tupelo.core tupelo.test))

(defn fib-recur
  [arg]
  (assert (and (int? arg) (pos? arg)))
  ; initialize state vars
  (loop [N      arg
         result 1]
    (if (zero? N)
      result
      ; compute next state vars
      (let [N-next      (dec N)
            result-next (* result N)]
        (recur N-next result-next))))) ; jump to top of loop with next state vars

(defn fib-while
  [arg]
  (assert (and (int? arg) (pos? arg)))
  ; initialize state vars
  (let [state (atom {:N      arg
                     :result 1})]
    (while (pos? (:N @state)) ; must use newest state value for N in test
      ; compute next state vars
      (let [N          (:N @state)
            result     (:result @state)
            state-next {:N      (dec N)
                        :result (* result N)}]
        (reset! state state-next))) ; save new state & jump to top of loop
    (:result @state)))

(dotest
  (is= 120 (fib-recur 5))
  (is= 120 (fib-while 5)))

如果在表达式后面的当前函数中没有其他要计算的内容,则该表达式位于尾部位置。在您的情况下,foobaz 中的所有 recur 调用都处于尾部位置,因此这两个函数都可以正常编译。

然而,

bar 中,recur 调用均不被允许,因为 expr-3expr-4 均不在尾部位置。有些函数调用使用每个 recur 调用的结果作为参数——也就是说,函数调用逻辑上跟在 recur 调用之后,因此 recur 不在尾部位置。

这并不是说你不能写 bar 来递归调用它自己,但你必须明确地编码它,如:

(defn bar [x]
 (if cond-expr-2
  (fn-1 (bar expr-3))
  (fn-2 (bar expr-4))))

这是绝对允许的,但是 bar 的这些递归调用将消耗堆栈 space 这意味着如果函数递归调用自身足够多次,您将 运行 出堆栈space。 recur(和一般的尾递归)很有价值,因为它不涉及进行传统意义上的函数调用——相反,(这里从逻辑上讲)堆栈上的函数参数被新参数和代码跳回到函数的开头,因此没有使用堆栈 space。当然,这意味着第一次调用函数时的原始参数丢失了。

其他版本的 Lisp 不使用 recur 关键字。当这些版本的 Lisp 发现函数正在递归调用自身时,它们会做出与 Clojure 相同的 "tail position" 判断,如果它们发现调用处于尾部位置,它们会执行与 Clojure 相同的 "replace-the-argument-and-jump" 逻辑,而如果他们发现调用不在尾部位置,他们会发出代码来执行 "real" 递归调用,而不是使编译失败。 Clojure 的优势在于,如果调用将被编译为尾递归调用(分支),开发人员可以一目了然。