SICP section 3.3.3
October 4th, 2007 at 12:58 pmThe code for this section is in Common Lisp.
The assoc function the authors define exists in Common Lisp, and the one-dimensional table presented here is called an association list, or alist.
Here’s a sample session:
[11]> (defvar tbl '((a . 1) (b . 2) (c . 3))) TBL [12]> (assoc 'c tbl) (C . 3) [13]> (assoc 'd tbl) NIL [14]> (assoc 'c tbl :test #'equalp) (C . 3)
As the last line shows, assoc accepts a test function as an argument (it uses eql by default).
Exercise 3.24
I’ll use the idiomatic CL way of keyword arguments to set same-key?. Keyword arguments are a very useful feature CL supports out-of-the-box.
(defun make-table (&key (same-key? #'eql))
(let ((local-table (list '*table*)))
(labels (
(tassoc (key table)
(assoc key table :test same-key?))
(lookup (key-1 key-2)
(let ((subtable (tassoc key-1 (cdr local-table))))
(if subtable
(let ((record (tassoc key-2 (cdr subtable))))
(if record
(cdr record)
nil))
nil)))
(insert! (key-1 key-2 val)
(let ((subtable (tassoc key-1 (cdr local-table))))
(if subtable
(let ((record (tassoc key-2 (cdr subtable))))
(if record
(setf (cdr record) val)
(setf (cdr subtable)
(cons (cons key-2 val)
(cdr subtable)))))
(setf (cdr local-table)
(cons (list key-1
(cons key-2 val))
(cdr local-table)))))
'ok)
(dispatch (m)
(case m
('lookup-proc #'lookup)
('insert-proc! #'insert!)
(otherwise (error "Bad dispatch ~a" m)))))
#'dispatch)))
(defvar operation-table (make-table :same-key? #'equal))
(defvar get (funcall operation-table 'lookup-proc))
(defvar put (funcall operation-table 'insert-proc!))
Here’s sample usage:
(funcall put 'x 'y 12) => OK (funcall get 'x 'y) => 12
Exercise 3.25
One simple and effective way to achieve this is to use a list as the key. You’ll also need to pass in an equality predicate that knows how to compare lists, for example equal.
Exercise 3.26
I’ll use the binary tree representation that was presented for sets in section 2.3.3 of the book. In the solution of exercise 2.66 I’ve already implemented a lookup, so what’s left is to package it all in a “local table” and add the insertion function.
; Generic binary tree
;
(defun make-tree (entry left right)
(list entry left right))
(defun make-leaf (entry)
(list entry nil nil))
(defun entry (tree)
(car tree))
(defun set-entry! (tree ent)
(setf (car tree) ent))
(defun left-branch (tree)
(cadr tree))
(defun set-left-branch! (tree lb)
(setf (cadr tree) lb))
(defun right-branch (tree)
(caddr tree))
(defun set-right-branch! (tree lb)
(setf (caddr tree) lb))
; Records
;
(defun make-record (key data)
(list key data))
(defun key (record)
(car record))
(defun data (record)
(cadr record))
; Table implemented as a binary tree
;
(defun make-table (&key (<? #'<))
(let ((local-table (cons '*head* nil)))
(labels (
(tree-root ()
(cdr local-table))
(set-tree-root! (node)
(setf (cdr local-table) node))
(node-lookup (key node)
(if (null node)
nil
(let* ((cur-entry (entry node))
(cur-key (key cur-entry)))
(cond ((funcall <? key cur-key)
(node-lookup
key
(left-branch node)))
((funcall <? cur-key key)
(node-lookup
key
(right-branch node)))
(t ; equal
cur-entry)))))
(lookup (key)
(node-lookup key (cdr local-table)))
(node-insert (key data node)
(let* ((cur-entry (entry node))
(cur-key (key cur-entry)))
(cond ((funcall <? key cur-key)
(if (null (left-branch node))
(set-left-branch!
node
(make-leaf
(make-record key data)))
(node-insert
key data (left-branch node))))
((funcall <? cur-key key)
(if (null (right-branch node))
(set-right-branch!
node
(make-leaf
(make-record key data)))
(node-insert
key data (right-branch node))))
(t ; equal
(set-entry!
node (make-record key data))))))
(insert! (key data)
(if (null (tree-root))
(set-tree-root!
(make-leaf (make-record key data)))
(node-insert key data (tree-root))))
(dispatch (m)
(case m
('lookup-proc #'lookup)
('insert-proc! #'insert!)
(otherwise (error "Bad dispatch ~a" m)))))
#'dispatch)))
Note the usage of a generic less than operator. This is an accepted technique which is used in, for example, the Standard Template Library of C++. By providing a less than operator, we can compare keys and infer their ordering (and even know whether they’re equal).
Here’s some sample code that demonstrates how this works:
(defvar my-t (make-table)) (defvar get (funcall my-t 'lookup-proc)) (defvar put (funcall my-t 'insert-proc!)) (funcall put 5 55) (funcall put 6 66) (funcall put 3 33) (funcall put 9 99) (funcall put 1 11) (funcall put 2 22) (funcall get 9) => (9 99) (funcall get 7) => NIL (funcall put 7 77) (funcall get 7) => (7 77)
Exercise 3.27
I won’t draw the env diagram, but I will explain what’s going on.
When memoize is called, and environment with the variable table is created, and the function returned points to this environment. Each time the function is executed (each call to memo-fib), it is executed within the body of the lambda that memoize returns – which checks for the value in the table.
(memo-fib n) takes n steps to compute its result because it never computes the same result twice. The call tree is flattened into a linear list.
The scheme would not work if we had defined memo-fib to be (memoize fib). This is because fib still calls itself (fib) instead of memo-fib in its recursive calls.
Related posts:

May 21st, 2008 at 17:26
(define memo-fib (memoize fib))
will not work. Subsequent calls to fib are not be saved and the algorithm runs in exponential time. I just checked it with DrSchema.
May 23rd, 2008 at 06:54
Pavel,
You’re right. I fixed the post with an explanation of why this doesn’t work.
Thanks
May 2nd, 2009 at 13:22
Hi,
.
I think you misread what is asked in exercise 3.25, the authors ask for a generalization of the two-dimensional tables, so to store ‘a under ‘(1 2 3) you must store ‘a under the key 3, then store the obtained table under the key 2, then store the obtained table under the key 1 in the main table. It is more complicated than just using the list as the key
[ And, compared to using the list as the key, it has the drawback that you can't store something under a list of keys that is a prefix/suffix of another list of keys under which something is already stored ; but this is just an exercise to make sure we understand how it works, and maybe it can be more convenient in some cases when we are sure that there are no prefixes/suffixes (the number of records in each table can be much lower than with lists as keys ). ]
May 8th, 2009 at 07:18
@F.D., why do you think the authors expected that? A list as the key is supposed to work as well, especially if the amount of dimensions is agreed upon in advance (and no prefix problem then occurs).
May 8th, 2009 at 19:56
Because the exercise asks us to “generalize one- and two-dimensional tables”, and in the book two-dimensional tables are implemented using a one-dimensional table in which the records themselves are one-dimensional tables. If you use a list as the key you have a one-dimensional table with lists as keys, not a generalization of the two-dimensional tables of the book. But I agree with you that using lists as keys works too, I just think it’s not what the authors expected.
The fact that the exercise says that “The lookup and insert! procedures should take as input a list of keys used to access the table.” is also confusing as to what is expected, but I think they request us to use lists only because we have not seen how to define functions that can take a variable number of arguments at this stage of the book.