如何使函数成为racket中函数的记忆版本

如何使函数成为racket中函数的记忆版本,racket,memoization,Racket,Memoization,我试图定义一个makememoize函数,它的参数是函数f。其思想是makememoize将返回一个与memoize一起运行的过程f。我已经能够在定义makememoize后返回一个过程,该过程使用函数f作为参数。然而,我还不能实际应用这个函数来表示一个数字的加、减或乘。也就是说,如果我将make memoize和add一个函数作为参数应用于数字28,那么结果应该是29 以下是我到目前为止得到的信息: (define (make-memoize f) (let ((memoized-valu

我试图定义一个makememoize函数,它的参数是函数f。其思想是makememoize将返回一个与memoize一起运行的过程f。我已经能够在定义makememoize后返回一个过程,该过程使用函数f作为参数。然而,我还不能实际应用这个函数来表示一个数字的加、减或乘。也就是说,如果我将make memoize和add一个函数作为参数应用于数字28,那么结果应该是29

以下是我到目前为止得到的信息:

(define (make-memoize f)
  (let ((memoized-values (make-hash)))
    (lambda (n)
      (if (hash-has-key? memoized-values n)
          (hash-ref memoized-values n)
          (f n)))))
当我使用函数add one to 28运行make memoize时:

(make-memoize (add-one 28))
这就是我得到的:

> (make-memoize (slow-add-one 28))
#<procedure:...s/rack-folder/test-file.rkt:26:4>
>(使记忆化(缓慢添加一个28))
#
它好像把程序和目录扔给我了?谢谢你的帮助。

我看到了几个问题:

  • 不使用计算值更新哈希表
  • make memoize
    是一个从函数创建新函数的函数
所以正确的使用是这样的:

(define (add-one n)
  (+ n 1))

(let ((fast-add-one (make-memoize add-one)))
  (fast-add-one 1)
  (fast-add-one 1)
  (fast-add-one 1))
下面提供了完整的代码,可以从
racketide
执行:

(define (add-one n)
  (+ n 1))

(define (make-memoize f)
  (let ((memoized-values (make-hash)))
    (lambda (n)
      (if (hash-has-key? memoized-values n)
          ;; Get and return the value from hash-table
          (let ((previous-value (hash-ref memoized-values n)))
            (printf "READ VALUE ~A->~A~%" n previous-value)
            previous-value)
          ;; Update the value in the hash table
          (let ((new-value  (f n)))
            (printf "SET  VALUE ~A->~A~%" n new-value)
            (hash-set! memoized-values n new-value)
            new-value)))))

(let ((fast-add-one (make-memoize add-one)))
  (fast-add-one 1)
  (fast-add-one 1)
  (fast-add-one 1))
评估结果应如下所示:

SET  VALUE 1->2 ;; Here, this is the first computation of add-one
READ VALUE 1->2 ;; Here, we just read from hash table
READ VALUE 1->2 ;; Here, we just read from hash table
编辑:错误问题的答案


记忆的最常见用途之一是减少递归过程调用中的计算量。即使修复了,单独发布的代码也不允许这样做。此外,将使用
make memoize
创建的过程绑定到新标识符将无效,因为所有递归调用中仍然使用未记忆的过程

对于原始发布的代码,给定一些键,目标是使用新键更新哈希表,除非该键已在表中找到(表示已进行并存储了计算。如果未找到键,则应计算该键的值,并将结果存储在表中。在任何一种情况下,都应返回与键关联的值。)

这是对刚才描述的内容的一个非常字面的转录:

(define (memo f)
  (let ((lookup (make-hash)))
    (lambda (x)
      (unless (hash-has-key? lookup x)
        (hash-set! lookup x (f x)))
      (hash-ref lookup x))))
这里,
memo
返回一个过程,当使用
x
调用该过程时,检查
lookup
x
。如果未找到
x
,则将其添加到
lookup
并与
(f x)
的值关联。最后,返回与
x
关联的值

let
-绑定仅适用于有限的情况 当
memo
-ized过程是递归的时,无法获得所需的效果。每个递归调用都使用
f
,而不是
memo
-ized版本的
f
,因此除了初始调用之外没有进一步的查找。例如,给定:

(define (fibonacci n)
  (cond ((= n 0) 0)
        ((= n 1) 1)
        (else (+ (fibonacci (- n 2))
                 (fibonacci (- n 1))))))
这将无法按预期工作:

(let ((fast-fib (memo fibonacci)))
  (fast-fib 40))
此处
fast fib
绑定到
memo
-ized过程,但递归调用调用
fibonacci
,因为这就是
fibonacci
的定义方式。这也不起作用:

(let ((fibonacci (memo fibonacci)))
  (fibonacci 40))
在这里,
fibonacci
是反弹到
memo
的过程,但是
fibonacci
在定义时称为
fibonacci
的原始版本,并且继续这样做

您需要找到一种方法来更改
fibonacci
的定义,使其本身成为
memo
-ized过程。您可以使用
set!
来完成此操作。您只需计算
(set!fibonacci(memo-fibonacci))
在使用
斐波那契
之前。最好有一个宏为您执行此操作:

(define-syntax-rule (memoize! f)
  (set! f (memo f)))
这是一个非常简单的宏,它简单地重新定义了给定的过程,使其成为
memo
-ized。下面是一些比较失败方法和成功方法的示例:

memoize.rkt> (time (fibonacci 40))
cpu time: 2780 real time: 2780 gc time: 0
102334155

memoize.rkt> (time (let ((fast-fib (memo fibonacci))) (fast-fib 40)))
cpu time: 2800 real time: 2800 gc time: 1
102334155

memoize.rkt> (time (let ((fibonacci (memo fibonacci))) (fibonacci 40)))
cpu time: 2789 real time: 2789 gc time: 0
102334155

memoize.rkt> (memoize! fibonacci)

memoize.rkt> (time (fibonacci 40))
cpu time: 0 real time: 0 gc time: 0
102334155
从上面可以看出,失败的方法根本没有改善
fibonacci
过程的运行时间;事实上,这些错误记忆的版本似乎比对
fibonacci
的裸调用慢一点。这是因为在上调用
memo
会产生额外的开销ode>fibonacci,它创建了一个只在初始调用时调用的无意义的记忆版本(所有后续调用实际上都在调用裸
fibonacci
过程)。但是,成功的记忆版本在递归调用时会调用自己,并且显示了相当多的改进

<> P>强调记忆化的价值,以及对错误的惩罚,考虑<代码>(斐波那契45)< /代码>。这似乎比以前的<代码>(Fibonacci 40)

略有增加。 由于正确记忆的版本会在调用之间缓存结果,因此我重新启动了REPL以进行下一个测试:

memoize.rkt> (memoize! fibonacci)

memoize.rkt> (time (fibonacci 1000))
cpu time: 1 real time: 1 gc time: 0
43466557686937456435688527675040625802564660517371780402481729089536555417949051890403879840079255169295922593080322634775209689623239873322471161642996440906533187938298969649928516003704476137795166849228875
fast-fib
版本计算
(fast-fib 40)
花费了近3秒,计算
(fast-fib 45)花费了31秒
。仅将输入值增加5,这是一个数量级的减速。然而,正确记忆的
斐波那契
版本计算
(斐波那契40)
所用的时间不到1微秒,计算
(斐波那契45)
所用的时间不到1微秒,计算
(fibonacci 1000)
(在所有三种情况下都从一个空的
查找表开始;在不清除缓存的情况下多次调用
fibonacci
时,性能会更好)。您需要等待非常长的时间才能完成
(fast fib 1000)

有很多方法可以改进这一点;您可能希望能够记忆多个参数的过程,或者您可能希望能够清除记忆过程的查找表,或者您可能希望能够取消记忆过程,等等。关于记忆的文献至少可以追溯到对于任何想深入研究的人来说,这都是20世纪60年代的事情。这个特别的话题是从另一个角度创建一个记忆化的过程
memoize.rkt> (time (fibonacci 40))
cpu time: 2780 real time: 2780 gc time: 0
102334155

memoize.rkt> (time (let ((fast-fib (memo fibonacci))) (fast-fib 40)))
cpu time: 2800 real time: 2800 gc time: 1
102334155

memoize.rkt> (time (let ((fibonacci (memo fibonacci))) (fibonacci 40)))
cpu time: 2789 real time: 2789 gc time: 0
102334155

memoize.rkt> (memoize! fibonacci)

memoize.rkt> (time (fibonacci 40))
cpu time: 0 real time: 0 gc time: 0
102334155
memoize.rkt> (time (let ((fast-fib (memo fibonacci))) (fast-fib 45)))
cpu time: 31042 real time: 31042 gc time: 11
1134903170

memoize.rkt> (memoize! fibonacci)

memoize.rkt> (time (fibonacci 45))
cpu time: 0 real time: 0 gc time: 0
1134903170
memoize.rkt> (memoize! fibonacci)

memoize.rkt> (time (fibonacci 1000))
cpu time: 1 real time: 1 gc time: 0
43466557686937456435688527675040625802564660517371780402481729089536555417949051890403879840079255169295922593080322634775209689623239873322471161642996440906533187938298969649928516003704476137795166849228875