【问题标题】:What exactly is tail position for recur?重复的尾部位置到底是什么?
【发布时间】:2011-10-18 20:47:22
【问题描述】:

Clojure 中recur 的“尾部位置”的精确定义是什么?我认为这将是循环 S 表达式中的最后一项,但在下面的示例中,在我看来,以 (if ...) 开头的 S 表达式位于尾部位置,即(循环 [绑定语句] [如果声明])。

(= __
  (loop [x 5
         result []]
    (if (> x 0)
      (recur (dec x) (conj result (+ 2 x)))
      result)))

(代码取自http://www.4clojure.com/problem/68

密切相关的问题:How can I call recur in an if conditional in Clojure?

【问题讨论】:

    标签: syntax clojure tail-recursion


    【解决方案1】:

    尾部位置是表达式将从其返回值的位置。尾部位置的表单被求值后,就没有更多的表单被求值了。

    考虑The Joy of Clojure的这个例子

    (defn absolute-value [x]
      (if (pos? x)
          x        ; "then" clause 
          (- x)))  ; "else" clause
    

    它接受一个参数并将其命名为 x。如果 x 已经是一个正数,那么 x 是 回来;否则返回 x 的反函数。 if 形式位于函数的尾部位置,因为无论它返回什么,整个 函数将返回。 “then”子句中的 x 也在函数的尾部位置。 但是“else”子句中的 x 不在函数的尾部位置,因为 x 的值 传递给-函数,不直接返回。整个 else 子句 (- x) 在 尾部位置。

    在表达式中类似

    (if a
        b
        c)
    

    bc 都位于尾部位置,因为它们中的任何一个都可以从 if 语句中返回。

    现在在你的例子中

    (loop [x 5
           result []]
      (if (> x 0)
        (recur (dec x) (conj result (+ 2 x)))
        result)))
    

    (if ...) 表单位于(loop ...) 表单的尾部,(recur ...)result 表单都位于(if ...) 表单的尾部。

    另一方面,在您链接的问题中

    (fn [coll] (let [tail (rest coll)]
                 (if (empty tail)
                     1
                     (+ 1 (recur tail)))))
    

    recur不是在尾部位置,因为 (+ 1 ...) 将在 (recur tail) 之后进行评估。因此 Clojure 编译器会报错。

    尾部位置很重要,因为您可以从尾部位置使用recur 表单。函数式编程语言通常使用递归来实现过程式编程语言通过循环完成的工作。但是递归是有问题的,因为它会消耗堆栈空间,而深度递归会导致堆栈溢出问题(除了速度很慢)。这个问题通常通过tail call optimization (TCO)来解决,当递归调用发生在函数/表单的尾部位置时,它会取消调用者。

    因为 Clojure 托管在 JVM 上,并且 JVM 不支持尾调用优化,所以它需要一个技巧来进行递归。 recur 形式就是这个技巧,它允许 Clojure 编译器执行类似于尾调用优化的操作。此外,它还验证recur 确实处于尾部位置。好处是您可以确保优化确实发生了。

    【讨论】:

    • 谢谢,很好的解释,我的困惑是我认为尾部位置是 S-Expression 中的一个位置,而不是最终评估。根据您的解释,似乎可以在循环中使用多个递归形式(例如在最终 IF s-exp 的两个分支中)对吗?
    • 是的,这完全有可能。我记得我也对此感到很困惑。此外,由于 JVM 上缺乏 TCO,我一直在阅读有关 recur 如何如此重要的信息……我不了解其中的第一件事(例如,TCO 代表什么……),我的头是旋转...
    • 是的,这很令人困惑,而且我从未在任何地方看到任何解释它的文档。也许我会建议他们根据您的回答在 recur 文档中添加一些内容。
    • @atc 我现在写它感觉很好;)谢谢!玩得开心!
    • 再次感谢您帮助新手了解尾递归。 :-)
    【解决方案2】:

    为了补充上面 Paul 的出色回答,Clojure 的乐趣 (ed1) 提供了一个表格(表 7.1),它以各种形式/表达式准确显示尾部位置的位置,我转载如下。查找“tail”一词在每个表单/表达式中出现的位置:

    |---------------------+-------------- ----------------+----------------| |表格 |尾部位置 |重复目标? | |---------------------+-------------- ----------------+----------------| | fn, 定义 | (fn [args] 表达式尾) |是 | |循环 | (循环[绑定]表达式尾)|是 | | let, letfn, 绑定 | (让 [绑定] 表达式尾部) |没有 | |做 | (做表达式尾巴)|没有 | |如果,如果不是 | (如果测试则尾,否则尾)|没有 | |什么时候,什么时候-不| (当测试表达式尾部时)|没有 | |条件 | (cond test test tail ... :else else tail) |没有 | |或者,和 | (或测试测试...尾) |没有 | |案例 | (case const const tail ... 默认tail) |没有 | |---------------------+-------------- ----------------+----------------|

    【讨论】:

      猜你喜欢
      • 2016-05-21
      • 2018-10-13
      • 1970-01-01
      • 2019-05-20
      • 2014-10-26
      • 1970-01-01
      • 1970-01-01
      • 2020-02-03
      • 2012-06-07
      相关资源
      最近更新 更多