编辑:我在编写本文的过程中发现了对自己问题的部分答案,但我认为可以轻松对其进行改进,因此无论如何我都会将其发布。也许那里有更好的解决方案?
我正在寻找一种简单的方法来定义let
形式的递归函数,而不必诉诸letfn
。这可能是一个不合理的请求,但是我正在寻找这种技术的原因是因为我混合了数据和递归函数,它们相互依赖,因此需要大量嵌套的let
和letfn
语句。
我想编写生成像这样的延迟序列的递归函数(以斐波那契序列为例):
(let [fibs (lazy-cat [0 1] (map + fibs (rest fibs)))]
(take 10 fibs))
但是在Clojure中,似乎
fibs
在绑定期间不能使用它自己的符号。解决它的明显方法是使用letfn
(letfn [(fibo [] (lazy-cat [0 1] (map + (fibo) (rest (fibo)))))]
(take 10 (fibo)))
但是,正如我之前所说,这导致许多麻烦的嵌套和
let
和letfn
的交替。为了在不使用
letfn
且仅使用let
的情况下执行此操作,我首先编写了一些使用我认为是U组合器的东西(今天刚刚听说过该概念):(let [fibs (fn [fi] (lazy-cat [0 1] (map + (fi fi) (rest (fi fi)))))]
(take 10 (fibs fibs)))
但是如何摆脱
(fi fi)
的冗余呢?正是在这一点上,我经过一个小时的苦苦挣扎并向组合器Q中逐渐添加了比特,才找到了自己问题的答案。
(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))
我用来定义递归序列的
Q
组合器叫什么?看起来像没有参数x
的Y组合器。一样吗(defn Y [r]
((fn [f] (f f))
(fn [y] (r (fn [x] ((y y) x))))))
clojure.core或clojure.contrib中是否还有提供Y或Q功能的另一个函数?我无法想象我只是做惯了...
最佳答案
莱特雷克
我最近为Clojure写了letrec
宏,这里是a Gist of it。它的作用类似于Scheme的letrec
(如果您碰巧知道的话),这意味着它是let
和letfn
之间的交叉:您可以将一组名称绑定到相互递归的值,而无需将这些值函数(也可以使用惰性序列),只要可以评估每个项目的标题而无需参考其他项目(这就是Haskell或类型理论的说法;这里的“ head”可能代表例如惰性序列对象本身,(至关重要!!不涉及任何强制)。
您可以使用它来编写类似
(letrec [fibs (lazy-cat [0 1] (map + fibs (rest fibs)))]
fibs)
通常只有在最高级别才有可能。有关更多示例,请参见要点。
正如问题文本中指出的那样,以上内容可以替换为
(letfn [(fibs [] (lazy-cat [0 1] (map + (fibs) (rest (fibs)))))]
(fibs))
对于相同的结果在指数时间内;
letrec
版本具有线性复杂度(顶级形式也是如此)。重复
自递归序列通常可以使用
(def fibs (lazy-cat [0 1] (map + fibs (rest fibs))))
构造-即,当固定范围的后视足以计算任何给定元素时。有关如何使用iterate
计算clojure.contrib.lazy-seqs
的示例,请参见fibs
。clojure.contrib.seq
iterate
提供了一个有趣的功能,称为c.c.seq
,可实现以下功能(take 10 (cseq/rec-seq fibs (map + fibs (rest fibs))))
它的局限性是只允许构造一个自递归序列,但是有可能从源头上提出一些实现思路,以实现更多不同的场景。如果您追求的是未在顶层定义的单个自递归序列,那么这必须是惯用的解决方案。
组合器
至于问题文本中显示的组合器,需要注意的是,它们受到JVM(以及Clojure)缺乏TCO(尾部调用优化)的阻碍,Clojure则选择直接将JVM的调用约定用于最高性能)。
顶层
还可以选择将相互递归的“事物”放在顶层,可能放在自己的名称空间中。如果需要以某种方式对那些“事物”进行参数化,则效果不佳,但是可以根据需要动态创建名称空间(有关实现思路,请参见
rec-seq
)。最后评论
我会很容易地承认
clojure.contrib.with-ns
远不是习惯性的Clojure,并且如果有其他事情可以做的话,我会避免在生产代码中使用它(而且总是有顶级选项...)。但是,玩(IMO!)感觉很好,并且看起来效果很好。我个人很想知道没有letrec
可以完成多少工作,以及letrec
宏在多大程度上使事情变得更容易/更清洁...我还没有对此发表意见。所以,就在这里。再一次,对于单个自递归seq情况,letrec
或contrib可能是最好的方法。