【问题标题】:recursion in implementation of "partition" function“分区”函数实现中的递归
【发布时间】:2014-01-11 22:13:00
【问题描述】:

我在随机阅读 Clojure 源代码时看到了 partition function was defined in terms of recursion without using "recur":

(defn partition
  ... ...
  ([n step coll]
     (lazy-seq
       (when-let [s (seq coll)]
         (let [p (doall (take n s))]
           (when (= n (count p))
             (cons p (partition n step (nthrest s step))))))))
  ... ...)

这样做有什么理由吗?

【问题讨论】:

    标签: recursion clojure tail-recursion lazy-sequences


    【解决方案1】:

    分区是惰性的。对partition 的递归调用发生在lazy-seq 的主体内。因此,它不会立即被调用,而是在一个特殊的 seq-able 对象中返回,以便在需要时进行评估并缓存迄今为止实现的结果。堆栈深度限制为一次调用一次。

    没有lazy-seq 的recur 可以用来创建一个急切的版本,但你不希望像在核心版本中那样在不确定长度的序列上使用它。

    【讨论】:

    • 我是这样理解你的回答的: 1. 结果可能是无限长的,所以我们更喜欢在这里让它变得懒惰。 2.如果是懒惰的,使用recur或者直接调用尾递归其实都是一样的。我理解正确吗?
    • 绝对不是:惰性和尾递归(使用recur)是产生序列的相互排斥的方式。如果您尝试在这里用recur 替换递归调用,它将无法编译,因为recur 不会在尾部位置。
    • @amalloy 嗨 amalloy: 1. 懒惰和recur 是互斥的,因为懒惰只评估一部分并一次重复评估所有? 2. 不明白为什么编译不出来?因为recur 的语法
    【解决方案2】:

    以@A.Webb 的回答和@amalloy 的评论为基础:

    recur 不是调用函数的简写,也不是函数。它是一种特殊的形式(在另一种语言中你会称之为语法)来执行尾调用优化。

    尾调用优化是一种允许在不破坏堆栈的情况下使用递归的技术(没有它,每个递归调用都会将其调用帧添加到堆栈中)。它还没有在 Java 中原生实现,这就是为什么 recur 在 Clojure 中用于标记尾调用的原因。

    使用lazy-seq 的递归是不同的,因为它通过将递归调用包装在一个闭包中来延迟递归调用。这意味着对根据lazy-seq(尤其是此类函数中的每个递归调用)实现的函数的调用(尤其是此类函数中的每个递归调用)会(立即)返回LazySeq序列,其计算会延迟,直到它被迭代。


    为了说明和限定@amalloy 的评论,即recur 和惰性是相互排斥的,下面是一个使用这两种技术的filter 的实现:

    (defn filter [pred coll]
      (letfn [(step [pred coll]
                (when-let [[x & more] (seq coll)]
                  (if (pred x)
                    (cons x (lazy-seq (step pred more))) ;; lazy recursive call
                    (recur pred more))))]                ;; tail call
        (lazy-seq (step pred coll))))
    
    (filter even? (range 10))
    ;; => (0 2 4 6 8)
    

    这两种技术可以用在同一个函数中,但不能用于同一个递归调用;如果对step 的惰性递归调用使用recur,则该函数将无法编译,因为在这种情况下recur 不会处于尾调用位置(尾调用的结果不会直接返回,而是会传递到lazy-seq)。

    【讨论】:

    • 感谢您澄清我的困惑!我真的希望 SO 允许我接受多个答案!
    【解决方案3】:

    所有的惰性函数都是这样写的。 partition 的这种实现将在没有调用 lazy-seq 的情况下破坏堆栈以获得足够大的序列。

    如果您对recur 的工作原理更感兴趣,请阅读有关 TCO(尾调用优化)的信息。当您使用尾递归时,这意味着您可以跳出当前的函数调用而不会丢失任何内容。在这种实现的情况下,您将无法这样做,因为您在下一次调用 partition 时是 cons-ing p。处于尾部位置意味着你是最后一个被叫到的东西。在实现中cons 处于尾部位置。 recur 只作用于尾部位置以保证 TCO。

    【讨论】:

    • 感谢您澄清我的困惑!我真的希望 SO 允许我接受多个答案!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-11-26
    • 2020-08-27
    • 2016-11-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多