我随机阅读了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))))))))
... ...)
有没有理由这样做?
答案 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。