使用定义的过程作为参数与使用 lambda 表达式作为参数产生不同的结果

Using a defined procedure as argument yields different result than using a lambda-expression as argument

我正在尝试记住方案中的一个过程。代码来自SICP

我的程序 fib 定义为

(define (fib n)
    (display "computing fib of ")
    (display n) (newline)
    (cond ((= n 0) 0)
        ((= n 1) 1)
        (else (+ (fib (- n 1))
                (fib (- n 2))))))

我的记忆过程如下

(define (memoize f)
    (let ((table (make-table)))
        (lambda (x)
            (let ((previously-computed-result (lookup x table)))
                (or previously-computed-result
                    (let ((result (f x)))
                       (insert! x result table)
                       result))))))

让我们定义两个过程

(define mem-fib (memoize fib))
(define mem-fib-lambda (memoize (lambda (n)
             (display "computing fib of ")
             (display n)
             (newline)
             (cond ((= n 0) 0)
               ((= n 1) 1)
                   (else (+ (memo-fib (- n 1))
                            (memo-fib (- n 2))))))))

如您所见,在 mem-fib 中,我使用 fib 作为参数,但在 mem-fib-lambda 中,我使用 lambda 表达式作为参数,这几乎是相同的。

使用 5 作为参数调用此过程会产生不同的结果,其中第一个 mem-fib 将最后一个结果存储在其 table 中,而 mem-fib-lambda 存储沿途的每个递归计算。

(mem-fib 5)
->computing fib of 5
->computing fib of 4
->computing fib of 3
->computing fib of 2
->computing fib of 1
->computing fib of 0
->computing fib of 1
->computing fib of 2
->computing fib of 1
->computing fib of 0
->computing fib of 3
->computing fib of 2
->computing fib of 1
->computing fib of 0
->computing fib of 1
->5
(mem-fib 5)
->5

(mem-fib-lambda 5)
->computing fib of 5
->computing fib of 4
->computing fib of 3
->computing fib of 2
->computing fib of 1
->computing fib of 0
->5
(mem-fib-lambda 5)
->5 

我的理论是,当我调用 mem-fib 时,fib 是在另一个环境中计算的,而 mem-fib-lambda 是在它被调用的环境中计算的。

为了解决这个问题,我尝试在记忆程序中复制一份

(define (memoize proc)
  (define f proc) ;; Here
    (let ((table (make-table)))
        (lambda (x)
            (let ((previously-computed-result (lookup x table)))
                (or previously-computed-result
                    (let ((result (f x)))
                       (insert! x result table)
                       result))))))

那没有用,所以我试着把它放在 let 表达式中。据我所知,fib 应该与 table

属于同一框架
(define (memoize proc)
    (let ((table (make-table))
         (f proc)) ;; Here
        (lambda (x)
            (let ((previously-computed-result (lookup x table)))
                (or previously-computed-result
                    (let ((result (f x)))
                       (insert! x result table)
                       result))))))

那也没有做任何事情。

我错过了什么?为什么行为会有所不同?我怎样才能得到我想要的结果?

谢谢

问题在于,在您的第一个函数中,您正在递归调用斐波那契的非记忆版本,而不是 fib 的记忆版本。 解决这个问题的方法是像这样定义 fib:

(define (fib n)
    (display "computing fib of ")
    (display n) (newline)
    (cond ((= n 0) 0)
        ((= n 1) 1)
        (else (+ (mem-fib (- n 1)) ;; Notice we're calling the memoized version here
                (mem-fib (- n 2))))))
(define mem-fib (memoize fib))

一个可以说更好的方法是执行以下操作:

(define (fib n)
    (display "computing fib of ")
    (display n) (newline)
    (cond ((= n 0) 0)
        ((= n 1) 1)
        (else (+ (fib (- n 1)) ;; Notice we're calling the NON-memoized version here
                (fib (- n 2))))))
(set! fib (memoize fib)) ;; but we redefine fib to be memoized

这意味着我们只使用一个名字并且它被记住了。没有同时使用两个版本的好方法,但如果您愿意,可以采用以下一种方法(如果您想比较性能或其他内容):

(define (fib n)
    (display "computing fib of ")
    (display n) (newline)
    (cond ((= n 0) 0)
        ((= n 1) 1)
        (else (+ (mem-fib (- n 1)) ;; Notice we're calling the memoized version here
                (mem-fib (- n 2))))))
(define mem-fib (memoize fib))
(set! fib (lambda (n)
    (display "computing fib of ")
    (display n) (newline)
    (cond ((= n 0) 0)
        ((= n 1) 1)
        (else (+ (fib (- n 1)) ;; Notice we're calling the NON-memoized version here
                (fib (- n 2)))))))

这是解决此问题的另一种方法。这是在 Racket 中,而不是在 Scheme 中,对此我深表歉意(它可能是 Scheme,除非我不知道哈希表在 Scheme 中是如何工作的)。

首先,这是一个 Racket 函数,用于在其第一个参数上记忆任意数量参数的函数。马上就会明白为什么我们需要允许额外的参数。

(define (memoize f (table (make-hasheqv)))
  ;; Memoize a function on its first argument.
  ;; table, if given, should be a mutable hashtable
  (λ (k . more)
    ;; hash-ref! looks up k in table, and if it is not there
    ;; sets it to be the result of calling the third argument (or
    ;; to the third argument if it's not callable).  thunk just makes
    ;; a function of no arguments.
    (hash-ref! table k
               (thunk (apply f k more)))))

现在有诀窍:不是将 fib 定义为递归函数,而是将 fib/c 定义为 递归函数,它知道如何做斐波那契数列计算的一步,然后将平底船转移到另一个函数来完成剩下的工作。它还会像您的函数一样告诉您它在做什么。

(define (fib/c n c)
  ;; fib/c does one step of the fibonacci calculation,
  ;; calling c to do the remaining steps.
  (printf "fib of ~A~%" n)
  (if (<= n 2)
      1
      (+ (c (- n 1) c)
         (c (- n 2) c))))

基于此我们可以很容易地定义 fib/u,一个未记忆的斐波那契函数,它具有您期望的糟糕性能,只需将 fib/c 本身作为其第二个参数传递:

(define (fib/u n)
  ;; unmemoized fib
  (fib/c n fib/c))

但现在我们可以记忆它,并定义一个记忆版本,fib/m(在这里你明白为什么我需要 memoize 来允许多个参数:我们需要继续传递记忆版本功能下降:

(define (fib/m n)
  ;; and here's a memoized fib
  (let ((fib/m (memoize fib/c)))
    (fib/m n fib/m)))

现在(4 是第一个不同的情况):

> (fib/u 4)
fib of 4
fib of 3
fib of 2
fib of 1
fib of 2
3
> (fib/m 4)
fib of 4
fib of 3
fib of 2
fib of 1
3

并删除印刷:

> (time (fib/u 40))
cpu time: 8025 real time: 7962 gc time: 26
102334155
> (time (fib/m 40))
cpu time: 1 real time: 1 gc time: 0
102334155

请注意,这种编写非递归函数并将其转换为递归函数的方法与 Y 的推导方式密切相关(尽管这些通常非常纯粹并且坚持使用具有只有一个参数,所以你最终得到 (λ (f) (λ (n) ... (f ...) ...)) 而不是 (λ (n c) ... (c ...) ...)。事实证明这是一个非常有用的技巧。