【问题标题】:Memoization with a closure example from Land of Lisp使用 Land of Lisp 的闭包示例进行记忆化
【发布时间】:2018-09-17 21:23:04
【问题描述】:

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. 我错过了一些东西。

【问题讨论】:

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


    【解决方案1】:

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

    一般来说,使用类似闭包的风格更可能出现在 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
    

    【讨论】:

    • 进一步阅读,我发现了一个类似于您在 Paul Graham 的 On Lisp 中建议的方法的想法,图 5.2。但我发现你的版本更容易使用,因为一个需要调用 (memoize 'foo) (foo 10) 而另一个需要调用类似 (setq fast-foo (memoize 'foo)) (funcall fast-foo 10 ) 除了这个和使用 equal 而不是 eql 进行比较之外,还有其他区别吗?
    猜你喜欢
    • 2011-11-18
    • 2016-02-11
    • 1970-01-01
    • 2022-11-03
    • 2019-08-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-10
    相关资源
    最近更新 更多