使用来自Land of Lisp的闭包示例进行记忆

时间:2018-04-08 09:59:06

标签: common-lisp memoization land-of-lisp

Land of Lisp 的第329页,Conrad Barski用以下示例代码解释了 memoization 的技术

(let ((old-neighbors (symbol-function 'neighbors))
      (previous (make-hash-table)))
  (defun neighbors (pos)
    (or (gethash pos previous)
        (setf (gethash pos previous) (funcall old-neighbors pos)))))

这个想法很好:当我调用neighbors函数时,我将结果保存到哈希表中,以便下次使用相同值neighbors调用pos时,我可以查看结果,而无需再次计算。

所以我想知道,通过编辑和重新编译它的源代码(在本书的第314页给出)将函数neighbors重命名为old-neighbors是否更容易。然后,memoization示例可以简化为

(let ((previous (make-hash-table)))
  (defun neighbors (pos)
    (or (gethash pos previous)
        (setf (gethash pos previous) (funcall old-neighbors pos)))))

或者,事先将previous转换为全局变量*previous-neighbors*,甚至进入

(defun neighbors (pos)
  (or (gethash pos *previous-neighbors*)
      (setf (gethash pos *previous-neighbors*) (funcall old-neighbors pos))))

因此不需要使用闭包。

所以我的问题是:这样做的原因是什么?

我能想象的原因:

  1. 这是教学的,显示了可以通过闭包(之前已经解释过)可以做什么并提供symbol-function的示例。
  2. 即使在您不能或不可以更改neighbors的源代码的情况下,此技术也适用。
  3. 我遗失了一些东西。

1 个答案:

答案 0 :(得分:7)

你已经做了一些很好的观察。

通常,使用类似闭包的样式更有可能在Scheme代码中找到 - Scheme开发人员经常使用函数来返回函数。

通常,如您所检测到的那样,让memoize函数foo调用函数old-foo是没有意义的。使用全局变量可以减少封装( - > 信息隐藏),但可以提高调试程序的能力,因为人们可以更轻松地检查记忆的价值观。

类似但可能更有用的模式是:

(defun foo (bar)
  <does some expensive computation>)

(memoize 'foo)

'memoize'会是这样的

(defun memoize (symbol)
  (let ((original-function (symbol-function symbol))
        (values            (make-hash-table)))
    (setf (symbol-function symbol)
          (lambda (arg &rest args)
            (or (gethash arg values)
                (setf (gethash arg values)
                      (apply original-function arg args)))))))

优点是您不需要为每个函数编写记忆代码。您只需要一个函数memoize。在这种情况下,闭包也是有意义的 - 尽管你也可以有一个存储memoize表的全局表。

请注意上述限制:比较使用EQL并且只使用函数的第一个参数进行记忆。

还有更广泛的工具可以提供类似的功能。

参见例如:

使用上面的memoize

CL-USER 22 > (defun foo (n)
               (sleep 3)
               (expt 2 n))
FOO

CL-USER 23 > (memoize 'foo)
#<Closure 1 subfunction of MEMOIZE 40600008EC>

使用arg 10的第一次调用运行三秒钟:

CL-USER 24 > (foo 10)
1024

使用arg 10的第二次调用运行得更快:

CL-USER 25 > (foo 10)
1024

使用arg 2的第一次调用运行三秒钟:

CL-USER 26 > (foo 2)
4

使用arg 2的第二次调用运行得更快:

CL-USER 27 > (foo 2)
4

使用arg 10的第三次调用快速运行:

CL-USER 28 > (foo 10)
1024