On page 329 of Land of Lisp, Conrad Barski explains the technique of memoization with the following example code
(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)))))
The idea is nice: when I call the neighbors
function, I save the result into a hash table, so that the next time calling neighbors
with the same value of pos
, I can just look-up the result without having to compute it again.
So I was wondering, whether it would not be easier to rename the function neighbors
to old-neighbors
by editing and recompiling its source code (given on page 314 of the book). Then the memoization example could be simplified to
(let ((previous (make-hash-table)))
(defun neighbors (pos)
(or (gethash pos previous)
(setf (gethash pos previous) (funcall old-neighbors pos)))))
or, by turning previous
into a global variable *previous-neighbors*
beforehand, even into
(defun neighbors (pos)
(or (gethash pos *previous-neighbors*)
(setf (gethash pos *previous-neighbors*) (funcall old-neighbors pos))))
thus rendering the closure unnecessary.
So my question is: what is the reason for doing it this way?
Reasons I could imagine:
symbol-function
.neighbors
.LISP Examples © Nick Parlante, 1996.Free for non-commerical use. 1) Write a function filter which takes a list and a predicate, and returns the list of the elements from the original list for which the predicate returns true. (There are actually LISP built-ins to do this called remove-if and remove-if-not t.
Memoization: Memoization is a technique for speeding up applications by caching the results of expensive function calls and returning them when the same inputs are used again. Let us try to understand this by breaking the definition into small parts.
At a high level, all memoization is doing is reducing the amount of work that your recursive algorithm would normally perform without it. It does this by storing the results of recursive calls in a hash table (aka. dictionary, or cache), and then retrieving them when they are needed.
The canonical example of memoization is applied to the Nth Fibonacci number. The prompt goes: Given some integer N, return the Nth number in the Fibonacci series. If you actually ran this code, chances are you wouldn’t be able to get a result for inputs greater than 50.
You have made some good observations.
Generally the style to use closures like that is more likely to be found in Scheme code - where Scheme developers often use functions to return functions.
Generally as you have detected it makes little sense to have a memoize function foo
calling an function old-foo
. Using global variables reduces encapsulation (-> information hiding), but increases the ability to debug a program, since one can more easily inspect the memoized values.
A similar, but potentially more useful, pattern would be this:
(defun foo (bar)
<does some expensive computation>)
(memoize 'foo)
Where ˋmemoizeˋ would be something like this
(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)))))))
The advantage is that you don't need to write the memoizing code for each function. You only need one function memoize
. In this case the closure also makes sense - though you could also have a global table storing memoize tables.
Note the limitations of above: the comparison uses EQL
and only the first argument of the function to memoize.
There are also more extensive tools to provide similar functionality.
See for example:
https://github.com/sharplispers/cormanlisp/blob/master/examples/memoize.lisp
https://github.com/AccelerationNet/function-cache
Using my memoize
from above:
CL-USER 22 > (defun foo (n)
(sleep 3)
(expt 2 n))
FOO
CL-USER 23 > (memoize 'foo)
#<Closure 1 subfunction of MEMOIZE 40600008EC>
The first call with arg 10
runs three seconds:
CL-USER 24 > (foo 10)
1024
The second call with arg 10
runs faster:
CL-USER 25 > (foo 10)
1024
The first call with arg 2
runs three seconds:
CL-USER 26 > (foo 2)
4
The second call with arg 2
runs faster:
CL-USER 27 > (foo 2)
4
The third call with arg 10
runs fast:
CL-USER 28 > (foo 10)
1024
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With