Memoization and closures in Scheme and Python
SICP exercise 3.27 discusses memoization in Scheme (get a large piece of paper if you want to draw the full environment diagram!). We make a couple of definitions
(define (fib n)
(if (<= n 1)
n
(+ (fib (- n 1)
(fib (- n 2))))))
and
(define (memoize f)
(lambda (n)
(cond ((in? n memo) (get n memo))
(else
(let ((result (f n)))
(add-kv! n result)
result)))))
(I’m simplifying the book’s code here - assume that we’ve made some kind of empty dictionary device called memo
which has membership testing with in?
, element access with get
, and add-kv!
to add a new key-value pair).
Of course, (define memo-fib (memoize fib))
is not the correctly memoized function we want. ‘Recursive’ calls it makes will actually be to the unmemoized fib
function, so after (memo-fib 10)
for example the memo will contain an entry for 10 but no other entries. The memoized version memo-fib
used in 3.27 is a function which is defined to be memoize
applied to a lambda function that calls memo-fib
. This doesn’t let us memoize functions we’ve already defined - we had to re-write the definition entirely.
To do that, we have to change the binding of the name fib
. Scheme lets us do this with (set! fib (memoize fib))
which works correctly. Imagine the environment diagram: after the set!
, there will be an environment E
used for the evaluation of (memoize fib)
in which the parameter f
is bound to the original definition of fib
(whose environment part is the global environment) and a return value which is an unnamed function whose definition is the body of (memoize f)
(and whose environment part is E
). In the global environment, fib
now points to this new function, so when we call (fib 3)
it refers to the new memoized code, which uses a function f
bound to the old definition of fib
, but the recursive calls in this old code use the name fib
which resolves to the new memoized function and thus the memoization works correctly.
This rebinding is exactly what happens with Python decorators:
@decorator
def f(x):
<body>
is equivalent to
def f(x):
<body>
f = decorator(f)
and indeed there is a memoizing decorator called cache
in functools
.
It’s more interesting to do the Python memoization by hand. Defining
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
memo = {}
def memoize(func):
def memoized_func(x):
if x in memo:
return memo[x]
value = func(x)
memo[x] = value
return value
return memoized_func
fib = memoize(fib)
is equivalent to what we did with set!
in Scheme, and pythontutor.com will draw you a beautiful frame diagram and step through the evaluation of fib(3)
. But we can now take the lid off and see what’s inside. Python functions have their closures accessible as dunder attributes: fib.__closure__[0]
is a cell object containing bindings for the free variable func
in memoized_func
when fib
was redefined, and we can get hold of this binding with
closure_fib = fib.__closure__[0].cell_contents
Self test: what will memo
look like after closure_fib(10)
is evaluated?
…
…
…
…
…
…
Answer: clo_fib
references the original fib definition, so memo[10]
will not be set. Recursive calls will use the name fib
referring to the memoized code, so all of memo[0]
to memo[9]
will be set.