【问题标题】:Clojure: Can only recur from tail positionClojure:只能从尾部位置重复
【发布时间】:2012-06-07 12:21:25
【问题描述】:

我正在尝试递归地反转列表,但在运行时得到Can only recur from tail position。这究竟意味着什么?如何改进我的代码以使其正常工作?

(defn recursive-reverse [coll]
  (loop [coll coll]
    (if (< (count coll) 2) '(coll)
      (conj (first coll) (recur (rest coll)))
      )))

编辑

Oscar 解决方案的输出。它适用于列表但不适用于向量?

user=> (= (recursive-reverse [1 2 3 4 5]) (recursive-reverse '(1 2 3 4 5)))
false
user=> (= '(1 2 3 4 5) [1 2 3 4 5])
true
user=> (recursive-reverse [1 2 3 4 5])
[1 2 3 4 5]
user=> (recursive-reverse '(1 2 3 4 5))
(5 4 3 2 1)

【问题讨论】:

  • 它对列表和向量的工作方式不同,因为conj 对两者的工作方式不同。将项目“添加”到列表的前面和向量的末尾是最便宜的。 conj 查看集合的类型并以最便宜的方式添加它。老实说,这对我来说从来都不是很正确,但我确信 Rich Hickey 和其他 Clojure 开发人员已经对此进行了非常彻底的思考,并认为利大于弊。
  • 令人着迷。我想性能是他们对基本操作的第一要义,但是是的,这似乎很奇怪。
  • 看准了,@Gert。我更新了我的答案,如果输入是向量,我宁愿返回不同类型的正确答案而不是错误答案

标签: recursion clojure


【解决方案1】:

错误Can only recur from tail position 表示您没有将recur 作为函数递归部分的最后一个表达式调用——事实上,在您的代码中conj 是最后一个表达式。

一些改进以使您的代码正常工作:

  • 询问集合是否为空作为基本情况,而不是比较其长度是否小于两个
  • conj 接收到一个 collection 作为它的第一个参数,而不是一个元素
  • 最好使用cons 而不是conj(根据documentation,根据集合的具体类型在不同位置添加新元素)。这样,如果输入集合是列表或向量,则返回的集合将被反转(尽管无论输入集合的类型如何,返回的集合的类型始终为clojure.lang.Cons
  • 请注意,'(coll) 是一个包含单个元素(符号 coll)的列表,不是实际集合
  • 为了正确反转列表,您需要遍历输入列表并将每个元素附加到输出列表的开头;为此使用累加器参数
  • 用于在函数的尾部位置利用尾递归调用recur;这样每次递归调用都会占用一定数量的空间,并且堆栈不会无限增长

我相信这就是您的目标:

(defn recursive-reverse [coll]
  (loop [coll coll
         acc  (empty coll)]
        (if (empty? coll)
            acc
            (recur (rest coll) (cons (first coll) acc)))))

【讨论】:

  • 感谢您的精彩回答。新谜团:为什么这适用于向量而不是列表?我在问题中发布了我的leon repl 输出。
  • 它的发生是因为conj 的工作方式:“根据具体类型,‘加法’可能发生在不同的‘地方’”。如果我们将conj 更改为cons(正如我在回答中所做的那样),返回的序列将始终反转,但集合的类型将是clojure.lang.Cons。看看这个post 的解释
【解决方案2】:

您只能从 Clojure 的尾部位置调用 recur。它是语言设计的一部分,是与 JVM 相关的限制。

你可以在不使用 recur 的情况下调用你的函数名(使用递归),并且取决于你如何构建你的程序,比如你是否使用惰性序列,你可能不会让堆栈爆炸。但是你最好使用 recur,并且使用带有 recur 的循环可以让你进行一些本地绑定。

这是来自4Clojure.com 的示例,其中使用递归而不使用递归。

(fn flt [coll]
  (let [l (first coll) r (next coll)]
    (concat 
      (if (sequential? l)
        (flt l)
        [l])
      (when (sequential? r)
        (flt r)))))

【讨论】:

    【解决方案3】:

    使您的代码工作的最简单方法是使用函数名而不是recur

    (defn recursive-reverse [coll]
      (loop [coll coll]
        (if (< (count coll) 2) '(coll)
          (conj (first coll) (recursive-reverse (rest coll)))
          )))
    

    当然,对于足够大的输入,它会使调用堆栈变大并出错。

    这是另一种编写尾调用友好版本的递归反向的方法:

    (defn recursive-reverse [s]
      (letfn [
        (my-reverse [s c]
          (if (empty? s)
            c
            (recur (rest s) (conj c (first s)))))]
        (my-reverse s '())))
    

    它从输入列表的头部拉出项目并将它们添加到累加器列表的头部。

    'Tail position' 只是意味着recur 是函数返回之前完成的最后一件事。这与您的“自然递归”解决方案不同,在该解决方案中,函数在调用自身和返回之间仍有工作要做。

    【讨论】:

    • 你为什么使用letfn而不是loop和两个参数?使用loop 更符合习惯。
    • @Gert:这只是另一种方法,我认为如果 OP 不熟悉循环,那么 letfn 可能更有意义。有惯用判断的来源吗?真的,我的理解是无论如何用累加器写这个都不是惯用的(基于Joy of Clojure)。
    • 这是一个很好的观点。但是loop 正是针对这种情况而存在的(创建一个不是函数调用本身的递归点),它也更简洁,被广泛使用,并在clojure.org/special_forms 上进行了说明/记录
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-08-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多