Как генерировать memoized рекурсивные функции в Clojure?

Я пытаюсь написать функцию, которая возвращает memoized рекурсивную функцию в Clojure, но у меня возникли проблемы с тем, чтобы рекурсивная функция видела свои собственные memoized привязки. Это потому, что var не создан? Кроме того, почему я не могу использовать memoize для локальной привязки, созданной с помощью let?

этот немного необычный производитель последовательности Фибоначчи, который начинается с определенного числа, является примером того, что я хотел бы сделать:

(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!?! 

Я могу, конечно, вручную написать макрос, который создает замкнутый-за Атома и управлять мемоизация себя, но я надеялся сделать это без такого шаманства.

8 ответов


Это, кажется, работает:

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


(def fib (memoize (fn [x] (if (< x 2)
                              x
                              (+ (fib (- x 1))
                                 (fib (- x 2)))))))
(time (fib 35))

есть интересный способ сделать это, который не полагается ни на перемотку, ни на поведение def. Главный трюк-обойти ограничения рекурсии, передав функцию в качестве аргумента себе:

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

     (partial mem-fib mem-fib)))

затем:

> ((make-fibo 1) 50)
20365011074

что происходит здесь:

  • на fib рекурсивная функция получила новый аргумент mem-fib. Это будет memoized версия , как только он будет определен.
  • в fib тело завернуто в let форма, которая переопределяет вызовы fib Так, что они передают mem-fib вниз к следующим уровням рекурсии.
  • mem-fib определяется как memoized fib
  • ... и пройдет мимо partial в качестве первого аргумента для себя, чтобы запустить вышеуказанный механизм.

этот трюк похож на тот, который используется комбинатором Y для вычисления точки фиксации функции при отсутствии встроенной рекурсии механизм.

учитывая, что def "видит" определяемый символ, нет никакой практической причины идти этим путем, за исключением, возможно, создания анонимных рекурсивных memoized функций на месте.


вот самое простое решение:

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

вы можете инкапсулировать рекурсивный шаблон memoized функции в макрос, если вы планируете использовать его несколько раз.

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

ваша первая версия действительно работает, но вы не получаете все преимущества memoization, потому что вы только один раз запускаете алгоритм.

попробуйте это:

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]

вот помесь Y-комбинатор и 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))))))))

вы можете macrosugar это:

(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

или расстояние Левенштейна:

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

вы можете генерировать memoized рекурсивные функции в Clojure с вариантом y-комбинатора. Например, код 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: рекурсивная мемуаризация в clojure.