如何在 Clojure 中生成记忆递归函数?

Posted

技术标签:

【中文标题】如何在 Clojure 中生成记忆递归函数?【英文标题】:How do I generate memoized recursive functions in Clojure? 【发布时间】:2011-04-23 20:07:26 【问题描述】:

我正在尝试在 Clojure 中编写一个返回记忆化递归函数的函数,但我无法让递归函数看到它自己的记忆化绑定。这是因为没有创建 var 吗?另外,为什么我不能在用 let 创建的本地绑定上使用 memoize?

这个从特定数字开始的稍微不寻常的斐波那契数列生成器就是我希望我能做的一个例子:

(defn make-fibo [y]
  (memoize (fn fib [x] (if (< x 2)
             y
             (+ (fib (- x 1))
                (fib (- x 2)))))))

(let [f (make-fibo 1)]
  (f 35)) ;; SLOW, not actually memoized

使用with-local-vars 似乎是正确的方法,但它也不适用于我。我想我不能关闭 vars?

(defn make-fibo [y]
  (with-local-vars [fib (fn [x] (if (< x 2)
                                  y
                                  (+ (@fib (- x 1))
                                     (@fib (- x 2)))))]
    (memoize fib)))

(let [f (make-fibo 1)]
  (f 35)) ;; Var null/null is unbound!?! 

我当然可以手动编写一个宏来创建一个封闭的原子并自己管理记忆,但我希望在没有这种黑客的情况下做到这一点。

【问题讨论】:

@Phelix和@CarlosNunes给出的解决方案在ClojureDocs page for memoize上。 【参考方案1】:

有一种有趣的方法可以做到这一点,它既不依赖于重新绑定也不依赖def 的行为。主要技巧是通过将函数作为参数传递给自身来绕过递归的限制:

(defn make-fibo [y]
  (let
    [fib
      (fn [mem-fib x]
         (let [fib (fn [a] (mem-fib mem-fib a))]
           (if (<= x 2)
             y
             (+ (fib (- x 1)) (fib (- x 2))))))
     mem-fib (memoize fib)]

     (partial mem-fib mem-fib)))

然后:

> ((make-fibo 1) 50)
12586269025

这里发生了什么:

fib 递归函数有一个新参数mem-fib。这将是 fib 本身的记忆版本,一旦它被定义。 fib 主体包裹在 let 表单中,该表单重新定义了对 fib 的调用,以便它们将 mem-fib 传递到下一层递归。 mem-fib 被定义为记忆化的fib ... 并将由partial 作为第一个参数传递给自身以启动上述机制。

这个技巧类似于 Y 组合器在没有内置递归机制的情况下用于计算函数的固定点的技巧。

鉴于def“看到”正在定义的符号,几乎没有实际理由这样做,除非可能是为了创建匿名的就地递归记忆函数。

【讨论】:

【参考方案2】:

这似乎有效:

(defn make-fibo [y]
  (with-local-vars
      [fib (memoize
            (fn [x]
              (if (< x 2)
                y
                (+ (fib (- x 2)) (fib (dec x))))))]
    (.bindRoot fib @fib)
    @fib))

with-local-vars 只为新创建的 Vars 提供线程本地绑定,一旦执行离开 with-local-vars 表单就会弹出;因此需要.bindRoot

【讨论】:

叮叮叮,谢谢,我们中奖了!但是为什么我们必须跳进javaland来做bindRoot呢?更重要的是,如果两个线程几乎同时执行 .bindRoot 操作,在退出此函数范围时变量被关闭之前,这不会造成并发风险吗?这对于同时创建生成的斐波那契函数仍然安全吗?或者 .bindRoot 是否以某种方式在词法范围内?我还是有点迷茫…… .bindRootsynchronized,但这在这里甚至无关紧要,因为我们在此时无法从任何其他线程访问的本地 Var 上调用它。至于方法调用的 Javaish 感觉,我相信这里是不可避免的(alter-var-root 不起作用,因为它需要 some 根绑定已经到位),但我不将此视为一个问题。如果有的话,我想知道我是否更愿意以某种不涉及本地变量的方式做同样的事情,但另一方面,这似乎是一种特别简单的方法...... 谢谢,我想我现在明白了。 bindRoot 调用创建了 var 的根绑定,但是这个绑定不与其他线程共享,因为它们有自己的 var 线程本地绑定,因此 var 的动态作用域不会让我们感到厌烦。此外,bindRoot 并不意味着 var 将从顶层可见。 根绑定通过memoize后面的机制从其他线程访问——然而,后者是线程安全的。 (但请参阅 Meikel Brandmeyer 的 this blog post,以深入分析 Clojure 中的记忆化和相关陷阱。)然而,除了 with-local-vars 表单的主体(它是 Var local 到该主体),因此在make-fibo 返回后无法以任何方式获取,除非通过调用返回的函数。 对于未来的读者...我将其提取到一个宏中:gist.github.com/1136161【参考方案3】:
(def fib (memoize (fn [x] (if (< x 2)
                              x
                              (+ (fib (- x 1))
                                 (fib (- x 2)))))))
(time (fib 35))

【讨论】:

如果您希望将 var 绑定在命名空间中,这是更典型的样式,但不幸的是您错误地更改了函数! y 参数怎么了?! (fib 2000) 给出了 ***Error。上面的例子没有使用递归,所以堆栈溢出是不可避免的,除非你通过调用函数来“热身”记忆 1 到 2000。但是你怎么知道 2000 对于任意用例来说足够大呢?这就是问题所在!【参考方案4】:

这是最简单的解决方案:

(def fibo
  (memoize (fn [n]
             (if (< n 2)
               n
               (+ (fibo (dec n))
                  (fibo (dec (dec n))))))))

【讨论】:

【参考方案5】:

如果您打算多次使用递归记忆函数模式,可以将其封装在一个宏中。

(defmacro defmemo
  [name & fdecl]
  `(def ~name
     (memoize (fn ~fdecl))))

【讨论】:

【参考方案6】:

这是 Y-combinator 和 Clojure 的 memoize 之间的交叉:

(defn Y-mem [f]
  (let [mem (atom )]
    (#(% %)
     (fn [x]
       (f #(if-let [e (find @mem %&)]
            (val e)
            (let [ret (apply (x x) %&)]
              (swap! mem assoc %& ret)
              ret))))))))

你可以把它放大:

(defmacro defrecfn [name args & body]
  `(def ~name
       (Y-mem (fn [foo#]
                 (fn ~args (let [~name foo#] ~@body))))))

现在使用它:

(defrecfn fib [n]
  (if (<= n 1)
      n
      (+' (fib (- n 1))
          (fib (- n 2)))))

user=> (time (fib 200))
"Elapsed time: 0.839868 msecs"
280571172992510140037611932413038677189525N

或者Levenshtein distance:

(defrecfn edit-dist [s1 s2]
  (cond (empty? s1) (count s2)
        (empty? s2) (count s1)
        :else (min (inc (edit-dist s1 (butlast s2)))
                   (inc (edit-dist (butlast s1) s2))
                   ((if (= (last s1) (last s2)) identity inc)
                      (edit-dist (butlast s1) (butlast s2))))))

【讨论】:

【参考方案7】:

您的第一个版本确实有效,但您并没有获得记忆化的所有好处,因为您只运行了一次算法。

试试这个:

user>  (time (let [f (make-fibo 1)]
          (f 35)))
"Elapsed time: 1317.64842 msecs"
14930352

user>  (time (let [f (make-fibo 1)]
          [(f 35) (f 35)]))
"Elapsed time: 1345.585041 msecs"
[14930352 14930352]

【讨论】:

它不能递归地工作,但这比仅仅缓存单个结束值更重要。【参考方案8】:

您可以使用 Y 组合器的变体在 Clojure 中生成记忆递归函数。例如,factorial 的代码是:

(def Ywrap
  (fn [wrapper-func f]
    ((fn [x]
       (x x))
     (fn [x]
       (f (wrapper-func (fn [y]
                      ((x x) y))))))))

 (defn memo-wrapper-generator [] 
   (let [hist (atom )]
    (fn [f]
      (fn [y]
        (if (find @hist y)
          (@hist y)
         (let [res (f y)]
           (swap! hist assoc y res)
        res))))))

(def Ymemo 
  (fn [f]
   (Ywrap (memo-wrapper-generator) f)))

(def factorial-gen
  (fn [func]
    (fn [n]
      (println n)
     (if (zero? n)
      1
      (* n (func (dec n)))))))

(def factorial-memo (Ymemo factorial-gen))

这在这篇关于Y combinator real life application: recursive memoization in clojure的文章中有详细解释。

【讨论】:

以上是关于如何在 Clojure 中生成记忆递归函数?的主要内容,如果未能解决你的问题,请参考以下文章

如何获得递归 CTE 中生成的最后一条记录?

如何在 Lisp 中记忆递归函数?

如何在 Clojure 中递归展平任意嵌套的向量和映射?

如何通过gen-class在Clojure中生成可序列化的java类?

Koltin 递归尾递归和记忆化

动态规划问题为什么要画表格?