Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Memoization with a closure example from Land of Lisp

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:

  1. It's didactical, showing what could be done with a closure (which had been explained just before) and providing an example of symbol-function.
  2. This technique is applicable even in situations, where you cannot or may not change the source code of neighbors.
  3. I am missing something.
like image 636
Dominik Mokriš Avatar asked Apr 08 '18 09:04

Dominik Mokriš


People also ask

What is an example of Lisp?

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.

What is memoization in JavaScript?

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.

How does memoization work in recursion?

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.

What is an example of memoization in Python?

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.


1 Answers

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/fare/fare-memoization
  • 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
like image 61
Rainer Joswig Avatar answered Jan 29 '23 21:01

Rainer Joswig