如何理解《Land of Lisp》329页闭包实现Memoization的示例代码?
Great question! Let's unpack this clever memoization snippet from Land of Lisp page 329—Conrad Barski does a nice job of using Common Lisp closures and hash tables to cut down on redundant function calls. Here's a step-by-step breakdown:
First, let's restate the full code for reference:
(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)))))
Step 1: The let Block - Capturing Private State
The outer let creates two local variables that the redefined neighbors function will "close over" (this is where the term closure comes from):
old-neighbors: This grabs the original implementation of theneighborsfunction using(symbol-function 'neighbors). We need to save this because we're about to redefineneighbors—we don't want to lose the core logic that actually calculates neighbors!previous: This initializes an empty hash table (make-hash-table) that will act as our cache. It maps position values (pos) to their precomputed neighbor lists.
Step 2: Redefining neighbors with Caching Logic
Inside the let, we overwrite the neighbors function to add memoization. The heart of the new function is this line:
(or (gethash pos previous) (setf (gethash pos previous) (funcall old-neighbors pos)))
This relies on Common Lisp's short-circuiting or behavior—here's what happens step by step:
- First, it checks if
posexists in theprevioushash table with(gethash pos previous). If the key exists,gethashreturns the cached neighbor list right away, and theorstops here. - If
posisn't in the cache,gethashreturnsnil. Theorthen moves to the second clause:- It runs the original
neighborslogic via(funcall old-neighbors pos)to compute the actual neighbor list forpos. - It stores this computed result in the hash table using
setf(so future calls for the sameposwill find it). - Finally, it returns the computed result (since
setfreturns the value it just stored).
- It runs the original
Why the Closure Makes This Work
The key trick here is that the redefined neighbors function keeps access to old-neighbors and previous even after the let block finishes running. These variables are private—no other code can mess with the cache or the original function logic accidentally. This means:
- The cache persists between calls to
neighbors—it doesn't get reset every time you invoke the function. - The original
neighborslogic is safely tucked away and only used when absolutely necessary.
Full Workflow Example
Let's walk through a real-world scenario to make this concrete:
- First call:
(neighbors '(1 2))gethashlooks for'(1 2)inprevious—it's not there, so returnsnil.funcall old-neighbors '(1 2)runs the original logic, say returning'((0 2) (1 1) (1 3) (2 2)).- This result is stored in
previousunder the key'(1 2). - The result is returned to the caller.
- Subsequent calls:
(neighbors '(1 2))gethashfinds'(1 2)inpreviousand returns the cached list immediately.- No need to run the original (potentially slow)
neighborslogic again—this is where the performance gain comes from!
内容的提问来源于stack exchange,提问作者Dominik Mokriš

