有没有更简单的方法来记忆递归let fn?

时间:2014-12-12 14:34:34

标签: clojure

假设你有一个let块中定义的递归函数:

(let [fib (fn fib [n]
            (if (< n 2)
              n
              (+ (fib (- n 1))
                 (fib (- n 2)))))]
  (fib 42))

这可以通过机械方式转换为使用memoize

  1. fn表单打包到memoize
  2. 将函数名称作为第一个参数移动。
  3. 将函数传递给自己,无论它在何处被调用。
  4. 使用partial重新绑定功能符号以执行相同操作。
  5. 转换上述代码会导致:

    (let [fib (memoize
                (fn [fib n]
                  (if (< n 2)
                    n
                    (+ (fib fib (- n 1))
                       (fib fib (- n 2))))))
          fib (partial fib fib)]
      (fib 42))
    

    这有效,但感觉过于复杂。问题是:有更简单的方法吗?

5 个答案:

答案 0 :(得分:3)

我冒风险回答,因为我不是学者,但我不这么认为。你几乎做了标准的事情,通过定点组合器可以部分应用memoization。

你可以尝试使用宏(对于简单的情况,它可能很容易,语法引用会为你做名称解析,你可以对它进行操作)。我回家后会试一试。

编辑:回到家里尝试了一些东西,对于简单的案例来说这似乎是好的

(defmacro memoize-rec [form]
  (let [[fn* fname params & body] form
        params-with-fname (vec (cons fname params))]
    `(let [f# (memoize (fn ~params-with-fname
                         (let [~fname (partial ~fname ~fname)] ~@body)))]
       (partial f# f#))))

;; (clojure.pprint/pprint (macroexpand '(memoize-rec (fn f [x] (str (f x))))))
((memoize-rec (fn fib [n]
                (if (< n 2)
                  n
                  (+ (fib (- n 1))
                     (fib (- n 2)))))) 75) ;; instant

((fn fib [n]
                (if (< n 2)
                  n
                  (+ (fib (- n 1))
                     (fib (- n 2))))) 75) ;; slooooooow

比我想象的简单!

答案 1 :(得分:1)

我不确定这是&#34;更简单&#34;本身,但我想我会分享一种方法,为我写的CPS变换器重新实现letfn

关键是引入变量,但延迟分配它们的值,直到它们都在范围内。基本上,你想写的是:

(let [f nil] (set! f (memoize (fn [] <body-of-f>))) (f))

当然这不起作用,因为let绑定在Clojure中是不可变的。但是,我们可以通过使用引用类型来解决这个问题 - 例如,promise

(let [f (promise)] (deliver! f (memoize (fn [] <body-of-f>))) (@f))

但这仍然不足,因为我们必须用f替换<body-of-f>(deref f)的每个实例。但是我们可以通过引入另一个调用存储在promise中的函数的函数来解决这个问题。所以整个解决方案可能如下所示:

(let [f* (promise)] (letfn [(f [] (@f*))] (deliver f* (memoize (fn [] <body-of-f>))) (f)))

如果你有一组相互递归的函数:

(let [f* (promise) g* (promise)] (letfn [(f [] (@f*)) (g [] (@g*))] (deliver f* (memoize (fn [] (g)))) (deliver g* (memoize (fn [] (f)))) (f)))

显然,这是很多锅炉板。但是构建一个提供letfn样式语法的宏是非常简单的。

答案 2 :(得分:1)

是的,有一种更简单的方法。 它不是功能转换,而是建立在clojure允许的杂质之上。

(defn fib [n]
  (if (< n 2)
    n
    (+ (#'fib (- n 1))
       (#'fib (- n 2)))))

(def fib (memoize fib))

第一步几乎以正常方式定义fib,但是使用进行递归调用var fib包含。然后重新定义fib,成为旧自我的备忘版本。

答案 3 :(得分:0)

我认为clojure惯用方法将使用recur

(def factorial
  (fn [n]
    (loop [cnt n acc 1]
      (if (zero? cnt)
           acc
         (recur (dec cnt) (* acc cnt))

答案 4 :(得分:0)

   ;; Memoized recursive function, a mash-up of memoize and fn
   (defmacro mrfn
      "Returns an anonymous function like `fn` but recursive calls to the given `name` within
      `body` use a memoized version of the function, potentially improving performance (see
      `memoize`).  Only simple argument symbols are supported, not varargs or destructing or
      multiple arities.  Memoized recursion requires explicit calls to `name` so the `body`
      should not use recur to the top level."
      [name args & body]
      {:pre [(simple-symbol? name) (vector? args) (seq args) (every? simple-symbol? args)]}
      (let [akey (if (= (count args) 1) (first args) args)]
        ;; name becomes extra arg to support recursive memoized calls
        `(let [f# (fn [~name ~@args] ~@body)
               mem# (atom {})]
           (fn mr# [~@args]
             (if-let [e# (find @mem# ~akey)]
               (val e#)
               (let [ret# (f# mr# ~@args)]
                 (swap! mem# assoc ~akey ret#)
                 ret#))))))


    ;; only change is fn to mrfn
    (let [fib (mrfn fib [n]
                (if (< n 2)
                  n
                  (+ (fib (- n 1))
                     (fib (- n 2)))))]
      (fib 42))

我的旧Mac上的时间:

原始时间:14089.417441毫秒

mrfn版本,经过的时间:0.220748毫秒