为了让大家了解背景,我正在学习Clojure和Lisp开发。 在学习Lisp的过程中,我目前正在通过“Little”系列努力巩固函数式编程和递归解决方案的基础。 在“The Little Schemer”中,我已经完成了许多练习,但是在将它们转换为Clojure时遇到了一些困难,尤其是在使用“recur”实现尾递归优化方面。 例如,这里是一个基于Clojure的“occurs*”函数实现(来自“The Little Schemer”),它计算出现在S表达式列表中的原子的数量:
(defn atom? [l]
(not (list? l)))
(defn occurs [a lst]
(cond
(empty? lst) 0
(atom? (first lst))
(cond
(= a (first lst)) (inc (occurs a (rest lst)))
true (occurs a (rest lst)))
true (+ (occurs a (first lst))
(occurs a (rest lst)))))
基本上,(occurs 'abc '(abc (def abc) (abc (abc def) (def (((((abc)))))))))
的结果为5。明显的问题是这个定义消耗了栈帧,如果给出一个深度太大的S表达式列表,它将会溢出栈。
现在,我明白将递归函数重构为使用累加器参数以启用将递归调用放入尾位置(以允许TCO)的选项,但我不确定是否适用于此类情况。
以下是我使用"recur"以及累加器参数尝试进行重构的程度:
(defn recur-occurs [a lst]
(letfn [(myoccurs [a lst count]
(cond
(empty? lst) 0
(atom? (first lst))
(cond
(= a (first lst)) (recur a (rest lst) (inc count))
true (recur a (rest lst) count))
true (+ (recur a (first lst) count)
(recur a (rest lst) count))))]
(myoccurs a lst 0)))
我感觉我快要成功了,但还差一点。显而易见的问题在于我的“else”语句中,列表的头部不是原子。从概念上讲,我想通过对列表中的第一个元素和剩余列表进行递归运算,然后将它们相加。我在思考如何重构代码以便将递归放到尾部位置。
除了使用“累加器”模式之外,还有哪些技术可以使递归调用处于尾部位置?或者,这个问题是否更“基本”,由于JVM缺乏尾部调用优化,Clojure并没有一个干净的解决方案呢?一般来说,Clojure程序需要递归处理S-表达式列表时应该采用什么通用模式?如果值得一提的话,我已经看到过使用多方法和lazy-seq技术(参考Halloway的《Programming Clojure》第151页)来“用惰性代替递归”的示例,但我不确定如何将该模式应用于这个例子中,因为我试图计算单个整数值,而不是构建一个列表。
非常感谢您提供的任何指导。