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

iva*_*var 37 recursion closures scope clojure memoization

我正在尝试编写一个在Clojure中返回memoized递归函数的函数,但是我在使递归函数看到它自己的memoized绑定时遇到了麻烦.这是因为没有创建var吗?另外,为什么我不能在用let创建的本地绑定上使用memoize?

这个稍微不同寻常的Fibonacci序列制作者从一个特定的数字开始就是我希望我能做的一个例子:

(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
Run Code Online (Sandbox Code Playgroud)

使用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!?! 
Run Code Online (Sandbox Code Playgroud)

我当然可以手动编写一个宏来创建一个封闭的原子并自己管理memoization,但我希望没有这样的hackery这样做.

Mic*_*zyk 19

这似乎有效:

(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))
Run Code Online (Sandbox Code Playgroud)

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

  • root绑定*可以通过`memoize`后面的机制从其他线程访问 - 但后者是线程安全的.(但请参阅Meikel Brandmeyer撰写的[此博客文章](http://kotka.de/blog/2010/03/memoize_done_right.html),深入分析Clojure及相关问题中的记忆.)Var不是,但是,除了`with-local-vars`形式的主体之外的任何地方都可以直接看到它(它是一个Var*local*到那个主体),所以在`make-fibo`返回之后不能以任何方式获得,除非通过调用返回的功能. (2认同)
  • 对于未来的读者......我将其解析为宏:https://gist.github.com/1136161 (2认同)

Phe*_*liX 18

(def fib (memoize (fn [x] (if (< x 2)
                              x
                              (+ (fib (- x 1))
                                 (fib (- x 2)))))))
(time (fib 35))
Run Code Online (Sandbox Code Playgroud)

  • `(fib 2000)`给出了StackOverflowError.上面的示例不使用recur,因此堆栈溢出是不可避免的,除非您通过调用1到2000的函数来"预热"memoization.但是,您怎么知道2000对于任意用例来说足够大?那就是擦! (3认同)

Raf*_*ird 18

有一种有趣的方法,既不依赖于重新绑定也不依赖于行为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)))
Run Code Online (Sandbox Code Playgroud)

然后:

> ((make-fibo 1) 50)
12586269025
Run Code Online (Sandbox Code Playgroud)

这里发生了什么:

  • fib递归函数有一个新的论点mem-fib.fib一旦定义,这将是自身的记忆版本.
  • fib身体包裹在一个let重新定义调用的形式fib,让他们传递mem-fib到递归的下一个水平.
  • mem-fib 被定义为memoized fib
  • ......并将partial作为第一个参数传递给自己以启动上述机制.

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

鉴于def"看到"正在定义的符号,除了可能用于创建匿名就地递归memoized函数之外,没有什么实际的理由去这种方式.