实现“分区”函数的递归

时间:2014-01-11 22:13:00

标签: recursion clojure tail-recursion lazy-sequences

我随机阅读了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))))))))
  ... ...)

有没有理由这样做?

3 个答案:

答案 0 :(得分:7)

分区是懒惰的。对partition的递归调用发生在lazy-seq的正文中。因此,它不会立即被调用,而是在需要时进行评估的特殊seq-able对象中返回,并缓存到目前为止实现的结果。堆栈深度限制为一次一次调用。

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

答案 1 :(得分:4)

建立@ 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)。

答案 2 :(得分:2)

所有惰性函数都是以这种方式编写的。 partition的这种实现会在没有调用lazy-seq的情况下对堆栈进行清理,以获得足够大的序列。

如果您对recur的工作方式更感兴趣,请阅读一些关于TCO(尾调用优化)的内容。当您使用尾递归时,这意味着您可以跳出当前的函数调用而不会丢失任何内容。对于此实现,您将无法执行此操作,因为您在cons的下一次调用时p正在partition。处于尾部位置意味着你是最后被召唤的东西。在实现中cons处于尾部位置。 recur仅适用于尾部位置以保证TCO。