在Clojure中,是否可以结合memoization和尾部调用优化?

时间:2012-03-27 21:34:53

标签: recursion clojure tail-recursion memoization

在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)但是我们不会从尾部调用优化中受益。

底线

有没有办法实现这两个目标:

  1. 尾调用优化
  2. 后续通话的中间结果的记忆
  3. 说明

    1. 此问题与Combine memoization and tail-recursion类似。但是那里的所有答案都与F#有关。在这里,我正在clojure寻找答案。
    2. The Joy of Clojure(第12.4章)将这个问题作为读者的练习。您可以在http://bit.ly/HkQrio
    3. 查阅该书的相关页面

5 个答案:

答案 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))))

使用trampolines

(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"