如何在Clojure中创建一个lazy-seq生成,匿名递归函数?

时间:2010-07-30 15:50:17

标签: binding recursion clojure combinators lazy-sequences

编辑:我在写这篇文章的过程中发现了对我自己的问题的部分答案,但我认为它很容易改进,所以无论如何我都会发布它。也许那里有更好的解决方案?

我正在寻找一种简单的方法来定义let形式的递归函数,而无需诉诸letfn。这可能是一个不合理的请求,但我寻找这种技术的原因是因为我混合了数据和递归函数,这些函数在某种程度上依赖于彼此需要大量嵌套let和{{1} }语句。

我想编写像这样生成延迟序列的递归函数(以Fibonacci序列为例):

letfn

但似乎在clojure (let [fibs (lazy-cat [0 1] (map + fibs (rest fibs)))] (take 10 fibs)) 在绑定期间不能使用它自己的符号。显而易见的方法是使用fibs

letfn

但正如我之前所说,这会导致许多繁琐的嵌套并交替(letfn [(fibo [] (lazy-cat [0 1] (map + (fibo) (rest (fibo)))))] (take 10 (fibo))) let

要在没有letfn并且只使用letfn的情况下执行此操作,我首先编写的内容使用了我认为的U-combinator(刚刚听说过这个概念):

let

但是如何摆脱(let [fibs (fn [fi] (lazy-cat [0 1] (map + (fi fi) (rest (fi fi)))))] (take 10 (fibs fibs))) 的冗余?

正是在这一点上,经过一个小时的挣扎并逐步向组合子Q添加位,我发现了自己问题的答案。

(fi fi)

这个(let [Q (fn [r] ((fn [f] (f f)) (fn [y] (r (fn [] (y y)))))) fibs (Q (fn [fi] (lazy-cat [0 1] (map + (fi) (rest (fi))))))] (take 10 fibs)) 组合器叫什么用于定义递归序列?它看起来像Y组合器,没有参数Q。它是一样的吗?

x

clojure.core或clojure.contrib中是否有另一个提供Y或Q功能的函数?我无法想象我刚才所做的是惯用的......

2 个答案:

答案 0 :(得分:11)

letrec

我最近为Clojure写了一个letrec宏,这里是a Gist of it。它就像Scheme的letrec(如果你碰巧知道的那样),这意味着它是letletfn之间的交叉:你可以将一组名称绑定到相互递归的值,而不是需要将这些值作为函数(惰性序列也可以),只要有可能在不参考其他项目的情况下评估每个项目的头部(即Haskell - 或者类型理论 - 用语;“head”)这可能代表懒惰的序列对象本身,至关重要! - 没有强制参与)。

您可以使用它来编写

之类的内容
(letrec [fibs (lazy-cat [0 1] (map + fibs (rest fibs)))]
  fibs)

通常只能在顶级。有关更多示例,请参阅Gist。

如问题文本所述,上述内容可以替换为

(letfn [(fibs [] (lazy-cat [0 1] (map + (fibs) (rest (fibs)))))]
  (fibs))

对于相同的结果在指数时间; letrec版本具有线性复杂度(与顶级(def fibs (lazy-cat [0 1] (map + fibs (rest fibs))))形式一样)。

迭代

自递归seqs通常可以用iterate构造 - 即当固定范围的后视足以计算任何给定元素时。有关如何使用clojure.contrib.lazy-seqs计算fibs的示例,请参阅iterate

clojure.contrib.seq

c.c.seq提供了一个名为rec-seq的有趣函数,支持

等功能
(take 10 (cseq/rec-seq fibs (map + fibs (rest fibs))))

它的局限性在于只允许构建一个自递归序列,但是有可能从它的源代码中提取一些实现思路,从而实现更多样化的场景。如果没有在顶层定义的单个自递归序列是您所追求的,那么这必须是惯用的解决方案。

组合子

对于问题文本中显示的组合器,重要的是要注意它们受到JVM上缺少TCO(尾调用优化)的阻碍(因此在Clojure中,它选择使用JVM的调用)惯例直接表现最佳表现。)

顶级

还可以选择将相互递归的“事物”置于顶层,可能在它们自己的命名空间中。如果需要以某种方式对这些“事物”进行参数化,这种方法就无法工作,但如果需要,可以动态创建名称空间(请参阅clojure.contrib.with-ns了解实现方法)。

最终评论

我很乐意承认letrec的东西远不是惯用的Clojure,如果还有别的东西可以做,我会避免在生产代码中使用它(因为总有顶级选项......)。但是,它(IMO!)很好玩,它似乎运作良好。我个人有兴趣了解在没有letrec的情况下可以完成多少工作以及letrec宏在多大程度上使事情变得更容易/更清洁......我还没有对此形成意见。所以,在这里。再一次,对于单个自递归seq情况,iterate或contrib可能是最好的方法。

答案 1 :(得分:8)

fn接受一个可选的name参数,该名称绑定到其正文中的函数。使用此功能,您可以将fibs写为:

(def fibs ((fn generator [a b] (lazy-seq (cons a (generator b (+ a b))))) 0 1))