在clojure中,我想编写一个尾递归函数,记住后续调用的中间结果。
[编辑:此问题已使用gcd
作为示例而不是factorial
重写。]
记忆gcd
(最大公约数)可以这样实现:
(def gcd (memoize (fn [a b]
(if (zero? b)
a
(recur b (mod a b))))
在此实施中,后续调用未记录中间结果。例如,为了计算gcd(9,6)
,gcd(6,3)
被称为中间结果。但是,gcd(6,3)
未存储在memoized函数的缓存中,因为recur
的递归点是未被记忆的匿名函数。
因此,如果在致电gcd(9,6)
之后,我们致电gcd(6,3)
,我们将无法从备忘录中受益。
我能想到的唯一解决方案是使用普通的递归(明确地调用gcd
而不是recur
)但是我们不会从尾部调用优化中受益。
有没有办法实现这两个目标:
F#
有关。在这里,我正在clojure
寻找答案。答案 0 :(得分:8)
在你的情况下很难显示memoize用factorial做任何事情,因为中间调用是唯一的,所以我会重写一个有点人为的例子,假设关键是要探索避免堆栈的方法:
(defn stack-popper [n i]
(if (< i n) (* i (stack-popper n (inc i))) 1))
然后可以从备忘录中获取一些东西:
(def stack-popper
(memoize (fn [n i] (if (< i n) (* i (stack-popper n (inc i))) 1))))
不吹风的一般方法是:
使用尾调用
(def stack-popper
(memoize (fn [n acc] (if (> n 1) (recur (dec n) (* acc (dec n))) acc))))
(def stack-popper
(memoize (fn [n acc]
(if (> n 1) #(stack-popper (dec n) (* acc (dec n))) acc))))
(trampoline (stack-popper 4 1))
使用懒惰序列
(reduce * (range 1 4))
这些都不是一直都有效,尽管我还没有找到一个没有工作的情况。我几乎总是首先去找懒惰的,因为我发现它们最像是clojure,然后我会用复发或抄写去尾调用
答案 1 :(得分:2)
(defmacro memofn
[name args & body]
`(let [cache# (atom {})]
(fn ~name [& args#]
(let [update-cache!# (fn update-cache!# [state# args#]
(if-not (contains? state# args#)
(assoc state# args#
(delay
(let [~args args#]
~@body)))
state#))]
(let [state# (swap! cache# update-cache!# args#)]
(-> state# (get args#) deref))))))
这将允许对memoized函数进行递归定义,该函数也会缓存中间结果。用法:
(def fib (memofn fib [n]
(case n
1 1
0 1
(+ (fib (dec n)) (fib (- n 2))))))
答案 2 :(得分:2)
(def gcd
(let [cache (atom {})]
(fn [a b]
@(or (@cache [a b])
(let [p (promise)]
(deliver p
(loop [a a b b]
(if-let [p2 (@cache [a b])]
@p2
(do
(swap! cache assoc [a b] p)
(if (zero? b)
a
(recur b (mod a b))))))))))))
有一些并发问题(双重评估,与memoize相同的问题,但由于承诺而更糟)可以使用@ kotarak的建议修复。
将上述代码转换为宏是留给读者的练习。 (Fogus的笔记非常简洁。)
把它变成一个宏在宏观上真是一个简单的练习,请注意身体(最后3行)保持不变。
答案 3 :(得分:0)
使用Clojure的重复,您可以使用没有堆栈增长的累加器编写阶乘,只需记住它:
(defn fact
([n]
(fact n 1))
([n acc]
(if (= 1 n)
acc
(recur (dec n)
(* n acc)))))
答案 4 :(得分:0)
这是使用 anonymous recursion 实施的阶乘函数, tail call 和 memoization 的中间结果。 memoization与函数集成在一起,对共享缓冲区的引用(使用Atom
引用类型实现)由 lexical closure 传递。
由于阶乘函数对自然数进行操作,并且成功结果的参数是递增的,Vector
似乎更适合存储缓冲结果的数据结构。
我们不是将先前计算的结果作为参数(累加器)传递,而是从缓冲区中获取它。
(def ! ; global variable referring to a function
(let [m (atom [1 1 2 6 24])] ; buffer of results
(fn [n] ; factorial function definition
(let [m-count (count @m)] ; number of results in a buffer
(if (< n m-count) ; do we have buffered result for n?
(nth @m n) ; · yes: return it
(loop [cur m-count] ; · no: compute it recursively
(let [r (*' (nth @m (dec cur)) cur)] ; new result
(swap! m assoc cur r) ; store the result
(if (= n cur) ; termination condition:
r ; · base case
(recur (inc cur)))))))))) ; · recursive case
(time (do (! 8000) nil)) ; => "Elapsed time: 154.280516 msecs"
(time (do (! 8001) nil)) ; => "Elapsed time: 0.100222 msecs"
(time (do (! 7999) nil)) ; => "Elapsed time: 0.090444 msecs"
(time (do (! 7999) nil)) ; => "Elapsed time: 0.055873 msecs"