Getting past yes or no

(We didn't talk about this in class today, but it picks up where we
left off on Wednesday.)

Sadly, the search function described in the previous chunk of
notes above doesn't tell me much---just whether or not an item 
I'm looking for is in the tree.  I'd get more information 
if I could get the search function to tell me how to 
get from the root of the tree to the item I'm looking for, 
assuming the item I'm looking for is in the tree.  
That path from the root to the item would at least be 
an approximation of the relationship between those two nodes 
in the tree; in the case of the Flintstones, for example, the 
path "Chip -has-dad-> Bam-Bam -has-dad-> Barney" tells me 
something about the relationship between Chip and Barney.  
How can I get my depth-first search procedure to 
return this path, instead of just the item itself, when it 
finds the item in the tree?  It's pretty easy.  All you do is 
introduce an additional argument as a sort of variable to 
store the path from the root to wherever the procedure is 
looking in the tree.  You get that additional argument by 
adding a helping function, just like in many of those examples
of tail recursion.  (But note that the addition of the helper
function does not make this depth-first search tail recursive.)
Then it's just a question of building up the result as the 
procedure searches deeper in the tree:

(defun dfs (item tree)
  (dfs-helper item tree nil))

(defun dfs-helper (item tree result)
  (cond ((done? tree) nil)
        ((eql item (data tree)) 
         (cons item result))
        (T (or (dfs-helper item 
                           (left-subtree tree)
                           (cons (data tree) result))
               (dfs-helper item 
                           (right-subtree tree)
                           (cons (data tree) result))))))

(defun done? (tree)
  (null tree))

(defun data (tree)
  (first tree))

(defun left-subtree (tree)
  (second tree))

(defun right-subtree (tree)
  (third tree))

And note that because I've taken the time to do a great deal 
of data abstraction, separating the functions that access the 
LISP data structure from the higher-level algorithm, that all 
I had to do was make a few changes to the top-level 
procedure; the lower-level ones are untouched because we 
didn't make any changes to the LISP data structure.


(Here's the part we did talk about)

Data abstraction and hw4

The database looks about like the thing below (abbreviated a bit):

   '((westley 
       ("the hero" tidbit)
       (inigo_montoya fezzik friend-of)
       (buttercup true-love-of))
     (buttercup
       ("the girl" tidbit)
       (westley true-love-of))
     ...

To think about data abstraction, it helps to give names to each "chunk"
of data that we're trying to abstract.  Looking at the small example
above, we can see how the structure "breaks down":

   the Database is a list of "Person"s
   each Person has a person-name and person-attributes
   each Attribute has an attribute-name and attribute-values

So we might label this kind of data

     (buttercup
       ("the girl" tidbit)
       (westley true-love-of))

a "Person", and then BUTTERCUP would be the person-name.  One Attribute
of this person would be:         ("the girl" tidbit)
which has an attribute-name of TIDBIT and attribute-values "the girl"
(there happens to be only one value for this attribute).


With that in mind, we can imagine some useful functions for data
abstraction might be

   (defun get-person (person-name database)
      "Given the database, get just one person's record"
   
   (defun get-person's-attribute (attribute-name person)
      "Given a record for one person, get the attribute named"
   
   (defun attribute-values (attribute)
      "Return the values associated with this attribute"

With these functions in hand, it is easy to answer questions like:
"Who are westley's friends?"

   > (attribute-values (get-person's-attribute 'FRIEND-OF
                                       (get-person 'WESTLEY *database*)))
   
   (INIGO_MONTOYA FEZZIK)

While the homework doesn't explicitly require you to write functions to
solve queries like that, it would be to your advantage, before jumping
into the whole assignment, to try to solve some easy queries like the
one above or similar ones like "What is INIGO's quote?"  With a little
work you can easily answer other questions, like "What actor plays the
role of the TRUE-LOVE-OF BUTTERCUP?" If you write some basic functions
for data abstraction at the beginning, these types of question should be
easy to answer, just by typing a little code at your LISP prompt.  This
will give you a decent way to test your functions, too.


Another thing to note about the code above: it's very easy to read and
follow.  If you're a masochist, you might prefer answering the queries
with code like this:

   > (but-last (find 'FRIEND-OF (rest (assoc 'WESTLEY *database*))
                     :key #'(lambda (x) (first (last x)))))

   (INIGO_MONTOYA FEZZIK)

but most of us wouldn't find that very comprehensible at all, now, would
we?  Writing code like that above is a recipe for GPA-disaster.  (Though
it does work as shown--try it in LISP and see!)


Some other similarities

You may remember from way back when we first started doing recursion
that we examined this function:

   ? (substitute 'x 'a '(a b (a) c a))
   (X B (A) C X)
   
   (defun my-substitute (new old input-list)
     (cond ((null input-list) nil)
           ((eql old (first input-list))
            (cons new (my-substitute new old (rest input-list))))
           (T (cons (first input-list)
                    (my-substitute new old (rest input-list))))))

It is important to note that the same recursive techniques we used to
write simple functions like SUBSTITUTE can also be used for big, complex
functions like those on homework four.  For example, let's take
ADD-PERSON.  It adds a new person to the database, and returns the
updated database.  

Recall that the "hard part" to this function is that
we must update the "backlinks".  So, if we were to add

   '((westley's-dad
        (westley father-of)
        ("Isn't in the movie" tidbit))

to the database, we'd have to figure out that SON-OF is the reciprocal
relationship of FATHER-OF, and update WESTLEY's info with the attribute

   (westley's-dad son-of)

in addition to his old info so that we wind up with

   '((westley 
       (westley's-dad son-of)
       ("the hero" tidbit)
       (inigo_montoya fezzik friend-of)
       (buttercup true-love-of))

starting with the info for WESTLEY we had before.

Looking back at SUBSTITUTE, we notice some similarities.  SUBSTITUTE
walks down a list, testing each element for a certain condition, and if
that condition is met, it replaces an element of the list with a new
one, leaving the other elements intact.  Similarly, ADD-PERSON must
walk down the list of people in the database, and replace entries that
need to be updated with their updates entries, leaving the other
entries intact.  Finally, ADD-PERSON must add the new person's data to
the front of the list.  So we might imagine something like

   (defun add-person (new-person database)
      "Adds person to front of list, then updates rest of
       the database's backpointers"
      (cons new-person (update-database new-person database)))


   (defun update-database (new-person database)
      "Works kind of like SUBSTITUTE"
   
            ;; if we reach the end, we're done
      (cond ((null database) nil)
   
            ;; if there are links from the new guy to this guy
            ((has-links new-person (person-name (first database)))
                 ;; replace this guy with an update copy
                 (cons (update-person new-person (first database))
                       (update-database new-person (rest database))))
   
            ;; else just copy this guy, and move on to the next guy
            (t (cons (first database)
                       (update-database new-person (rest database))))))
   
   (defun update-person (new-person old-person)
      "Returns copy of old-person with attributes added for backlinks
       to new-person"
      ...)

That code might have bugs, or it might not be the best strategy or
whatever--I don't know.  It was just an idea I had.  We might instead
try and write something totally different and use applicative
programming, as in

   (defun update-database (new-person database)
      "New applicative version"
      (mapcar #'(lambda (old-person) 
                   (update-person new-person old-person))
              database))

If you're comfortable with applicative programming, that might result in
a nice, short solution.  If MAPCAR still gives you the heebie-jeebies,
you can stick with a recursive solution.  There's probably more than one
(or even two!) decent ways to attack the problem.  But I guarantee that
all of the "good" ways utilize lots of abstraction.


Lecture notes by Kurt Eiselt, 1998.
Additions by Brian McNamara, 1998.
Last updated on Fri Jul 24 12:12:07 EDT 1998 by Brian McNamara