Laboratory Exercises For Computer Science 261

Still More LISP

Still More LISP

Goals: This laboratory exercise continues an introduction of LISP, considering local variables, iteration, box-and-pointer notationand mapping).

Local Variables and Scope

So far we've seen three ways in which a value can be associated with a variable in LISP:

The first part of this lab reviews two new, but related, mechanisms to perform this association:

Let

A let-expression in LISP is an alternative way to create local bindings. A let-expression contains a binding list and a body. The body can be any expression, or sequence of expressions, to be evaluated with the help of the local variable bindings. The binding list is a pair of parentheses enclosing zero or more binding specifications; a binding specification, in turn, is a pair of parentheses enclosing a variable and an expression. Here's an example of a binding list:

((next (car source)) (char-list '()))

This binding list contains two binding specifications -- one in which the value of the expression (car source) is bound to the symbol next, and the other in which the empty list is bound to the symbol char-list. Notice that binding lists and binding specifications are not procedure calls; their role in a let-expression is structural.

When a let-expression is evaluated, the first thing that happens is that the expressions in all of its binding specifications are evaluated and collected. Then the symbols in the binding specifications are bound to those values. Next, the expressions making up the body of the let-expression are evaluated, in order; the value of the last expression in the body becomes the value of the entire let-expression. Finally, the local bindings of the variables are canceled. (Variables that were unbound before the let-expression become unbound again; variables that had different bindings before the let-expression resume those earlier bindings.)

  1. What are the values of the following let-expressions?

    1. (let ((tone "fa") (call-me "al"))
        (concatenate 'string call-me tone "l" tone))
      
    2. ;; solving the quadratic equation x^2 - 5x + 4
      ;;
      (let ((discriminant (- (* -5 -5) (* 4 1 4))))
        (list (/ (+ (- -5) (sqrt discriminant)) (* 2 1))
              (/ (- (- -5) (sqrt discriminant)) (* 2 1))))
      
    3. (let ((sum (+ 8 3 4 2 7)))
        (let ((mean (/ sum 5)))
          (* mean mean)))
      

    You may use Allegro Common LISP to help you answer these questions, but be sure you can explain how it arrived at its answers.

Using a let-expression often simplifies an expression that contains two or more occurrences of the same subexpression. The programmer can compute the value of the subexpression just once, bind a variable to it, and then use that variable whenever the value is needed again. Sometimes this speeds things up by avoiding such redundancies as the recomputation of the discriminant in 1(b) above; in other cases, there is little difference in speed, but the code may be a little clearer. For instance,consider the following remove-all procedure that deletes all occurrences of an item from a list:

(defun remove-all (item ls)
    (if (null ls)
        '()
        (let ((first-element (car ls))
              (rest-of-result (remove-all item (cdr ls))))
          (cond ((equal first-element item) rest-of-result)
                ((listp first-element)
                 (cons (remove-all item first-element) rest-of-result))
                (else (cons first-element rest-of-result))))))

In this procedure, the let allows us to evaluate (car ls) and (remove-all item (cdr ls)) just once. Giving each value a name also makes it a little easier to understand what the three cond-clauses are doing.

Consider the following count-all-symbols procedure which counts the number of symbols that appear in a list or any of its sublists. A sample run also is given:

(defun count-all-symbols (ls)
    (cond ((null ls) 0)
          ((symbolp (car ls)) (+ 1 (count-all-symbols (cdr ls))))
          ((listp (car ls))
           (+ (count-all-symbols (car ls)) (count-all-symbols (cdr ls))))
          (else (count-all-symbols (cdr ls)))))

(count-all-symbols '(((a b) c) d (e (f)))) ===> 6
  1. Rewrite this count-all-symbols procedure, using a let-expression to consolidate repeated subexpressions in the same manner.

Let*

Example 1c above shows that it is possible to nest one let-expression inside another. One might be tempted to try to combine the binding lists for the nested let-expressions, thus:

;; Combining the binding lists doesn't work!
;;
(let ((sum (+ 8 3 4 2 7))
      (mean (/ sum 5)))
  (* mean mean))

This wouldn't work (try it and see!), because, within a let-expression, all of the expressions are evaluated before any of the variables are bound. Specifically, LISP will try to evaluate both (+ 8 3 4 2 7) and (/ sum 5) before binding either of the variables sum and mean; since (/ sum 5) can't be computed until sum has a value, an error occurs. You have to think of the local bindings coming into existence simultaneously rather than one at a time.

Because one often needs sequential rather than simultaneous binding, LISP provides a variant of the let-expression that rearranges the order of events: If one writes let* rather than let, each binding specification in the binding list is completely processed before the next one is taken up:

;; Using let* instead of let works!
;;
(let* ((sum (+ 8 3 4 2 7))
       (mean (/ sum 5)))
  (* mean mean))

The star in the symbol let* has nothing to do with multiplication; just think of it as an oddly shaped letter.

  1. Write a nested let-expression that binds a total of five variables, a, b, c, d, and e, with a bound to 9387 and each subsequent variable bound to a value twice as large as the one before it -- b should be twice as large as a, c twice as large as b, and so on. The body of the innermost let-expression should compute the sum of the values of the five variables.

  2. Write a let*-expression equivalent to the let-expression in the previous exercise.

One can use a let- or let*-expression to create a local name for a procedure:

(defun hypotenuse-of-right-triangle (first-leg second-leg)
    (let ((square (lambda (n)
                    (* n n))))
      (sqrt (+ (square first-leg) (square second-leg)))))

Regardless of whether square is defined outside this procedure, the local binding gives it the appropriate meaning in the body of the let-expression.

  1. Review the following code to determine what is printed:

    [user]: (setf x 'outside)
    [user]: (let ((x 'inside)
                  (y x))
                 (list x y))
                 
    [user]: (let* ((x 'inside)
                  (y x))
                 (list x y))
    
    Be sure you understand the result of each of these statements

  2. Consider the following function which returns the longer of two strings:
    (define longer-string (str1 str2)
        (if (< (string-length str1) (string-length str2))
            str2
            str1))
    
    Use this function in the definition of a procedure longest-on-list which takes as its argument any non-empty list of character strings and returns whichever element of that list is the longest:
    (longest-on-list '("This" "is" "the" "forest" "primeval")) ===> "primeval"
    (longest-on-list '("Wherefore" "art" "thou" "Romeo")) ===> "Wherefore"
    (longest-on-list '("To" "be" "or" "not" "to" "be")) ===> "not"
    (longest-on-list '("foo")) ===> "foo"
    
    Feel free to use let or let* as needed.

Iteration: do-expressions

As in procedural programming in other languages, LISP allows us to perform some procedure call or sequence of procedure calls repeatedly, for the sake of the side effects on structures or variables or for input or output. For example, the printing of the table of numbers and their square roots involves the repeated output of a line of a number and its root. LISP provides a special expression type to capture this form concisely and efficiently: the do-expression. A do-expression has the following structure:

(do loop-control-list
    (exit-test postlude)
   body)

Here's a simple example of a do-expression. We write a procedure display-countdown that takes one argument, a non-negative integer, and prints out the positive integers equal to or less than its argument, in descending order, one per line. Thus, the interaction might look as follows:

[user]: (display-countdown 3)
3
2
1
Blast off!
The following code demonstrates a simple solution to this problem:
(defun display-countdown (start)
    (do ((remaining start (- remaining 1)))
        ((zerop remaining) "Blast off!")
      (print remaining)))

Let's trace through the execution of a call to this procedure, using the above example (display-countdown 3):

  1. Write a procedure display-count that takes two arguments, start and finish, and counts upwards from start to finish, displaying each number on a separate line. (The preconditions are that start and finish must both be exact integers and start must be less than or equal to finish.) The display-count procedure should return the number of lines of output it produces.

  2. Write a sqrt-table procedure that takes one argument, a non-negative integer, and prints out a table of integers and their square roots, for integers equal to or less than its argument. A sample interaction follows:
    [user]: (sqrt-table 5)
    Number   Square Root
       1       1.00000
       2       1.41421
       3       1.73205
       4       2.00000
       5       2.23607
    
    Use a do-expression to control the repeated computations.

In the next example of a do-expressions, a procedure takes a list as argument and returns the list in reverse order.

(defun reverse-list (ls)
    (if (listp ls)
        (do ((new-ls '() (cons (car old-ls) new-ls))
             (old-ls ls (cdr old-ls)))
            ((null old-ls) new-ls)
        )
    )
)

[user]: (reverse-list '(3 1 4 1 5 9))
(9 5 1 4 1 3)

This time there are two loop-control variables: new-ls provides a list of elements already processed (starting with nil, while old-ls contains the list elements not yet processed. The iteration moves an element from old-ls to new-ls as part of the updating of variables. the body of the do-expression is null.

  1. Define a LISP procedure that takes any non-empty list of real numbers as its argument and returns the number that is the greatest element of the list. Use a do-expression to run through the positions of the list.

  2. Define a sum procedure, which takes any list of numbers as its argument and returns their sum. In your procedure, use a do-expression to process list elements iteratively.

Overall, Do-expressions can be used for clarity and concision in many of the situations.

Box and Pointer Representation

As we have seen, LISP uses cons to build lists. The result of a cons procedure commonly is represented as a rectangle - divided in half. From the first half of the rectangle, we draw an arrow to the head of a list; from the second half of the rectangle, we draw an arrow to the rest of the list. For example, (cons 'a '()) would be represented as follows:

Here, the line to a indicates that this is the head of the list. The diagonal line through the right half of the rectangle indicates that nothing comes later in this list. Since (cons 'a '()) gives the list (a), this diagram represents (a) as well.

Now consider the list (cons 'b '(a)) or (b a). Here, we draw another rectangle, where the head points to b and the tail points to the representation of (a) that we already have seen. The result is:

Similarly, the list (d c b a) is constructed as
(cons 'd (cons 'c (cons 'b (cons 'a '())))) and would be drawn as follows:

A similar approach may be used for lists, which have components which are sublists. For example, consider the list ((a) b (c d) e) This is a list with four components, so at the top level we will need four rectangles, just as in the previous example for the list (d c b a). Here, however, the first component designates the list (a), which itself involves the box-and-pointer diagram already discussed. Similarly, the list (c d) has two boxes for its two components (just as we discussed for (b a) earlier). The resulting diagram follows:

Throughout these diagrams, the null list is represented by a null pointer or line. Thus, the list containing the null list, ( ( ) ) - that is (cons '() '()) - is represented by a rectangle with lines through both halves:

  1. Draw box-and-pointer diagrams for each of the following lists:
    
       ((x) y z)
       (x (y z))
       ((a) b (c ()))
    
While we consistently have discussed cons in the context of lists, LISP allows cons to be applied when the second element is not a list. For example, (cons 'a 'b) is a legal expression which can be represented by the following box-and-pointer diagram:

To represent such an expression on paper or as a LISP printout, dot notation is used: (a . b) Here, the dot indicates that cons has been applied, but the second argument is not a list. Similarly,
(cons 1 'a) may be written (1 . a) and (cons "Henry" "Walker") produces
("Henry" . "Walker"). Using a box-and-pointer representation, this last result would be drawn as follows:

  1. Enter the following into LISP. In each case, explain when the dot notation appears or why it is not needed.
    
       (cons 'a "Walker")
       (cons 'a '())
       (cons '() 'a)
       (cons '() '())
    
  2. Draw each of the expressions in the previous step using the box-and-pointer representation.

Mapping: Assocication Lists

Consider the organization in a simple telephone book. Overall, a telephone directory consists of a sequence of entries, and each entry in the book includes a name and a telephone number.

One way to write such a directory in LISP is to consider each entry as a two-element list, such as ("walker" 4208) or ("stone" 3181). An entire directory, then, could be considered as a list of such entries:


   (  ("herman"  4202)  ("stone"   3181)  ("walker"  4208) )
In LISP, such a list of pairs is called an association list or alist.

As the telephone directory example illustrates, a particularly common application of association lists involves looking for a desired name or first element of a pair and retrieving the second element of a pair. Thus, the first element of each pair (the car of a pair) often is called a key value, and the remained or the pair is its associated data. For example, in the above illustration, "herman", "stone", "walker" are the keys, and the numbers are the associated data. As this example suggests, association lists are a simple way to implement small databases.

Since such applications are so common, LISP provides procedures to retrieve a pair containing a desired key. The most frequently used such procedure is assoc. Given a key and association list, assoc returns the first pair with the given key. If the key does not match any key, then assoc returns false (#f). For example,


   (assoc 'stone '((herman 4202) (stone 3181) (walker 4208)))
returns (stone 3181), while .

   (assoc 'jepsen '((herman 4202) (stone 3181) (walker 4208)))
returns NIL.

Since such directories typically contain several entries, this is a particularly good circumstance to use a setf expression, so a symbol can represent the entire directory:


   (setf telephone-directory
      '(  (herman  4202)  
          (stone   3181)  
          (walker  4208) 
       )
   )
With such a definition, (assoc 'stone telephone-directory) returns (stone 3181), and (assoc 'jepsen telephone-directory) returns NIL.

To find the telephone number corresponding to a given name, we could apply the cadr procedure to the result of assoc:


   (cadr (assoc 'stone telephone-directory))
returns 3181.

  1. Define an association list birthdays which associates peoples' names (as strings) with their birthdays (again, as strings). Thus, a typical entry might be
    (Lincoln "February 12, 1809") or (Lincoln (February 12 1809)).

  2. Use the assoc procedure to search this association list for someone who is on the list and for someone who is not on the list.

LISP contains at least four notions of equality for use in testing: For most LISP interpreters, assoc uses eql in its test of equality, so that keys involving strings will never match in a look-up. For example, (assoc "Herman" directory) could never find a match, regardless of how directory was defined, as "Herman" would never satisfy an eql test.)

To change the test for equality, you can specify the predicate as part of the call to assoc to be (assoc "Herman" telephone-directory :test #'equal).

  1. Define telephone-directory with the faculty names as strings:
    setf telephone-directory
          '(  ("herman"  4202)  
              ("stone"   3181)  
              ("walker"  4208) 
           )
       )
    
    Now, observe the results from the following statements:
    [user]: (assoc "herman" telephone-directory)
    [user]: (assoc "herman" telephone-directory :test #'equal)
    [user]: (assoc "Herman" telephone-directory :test #'equal)
    [user]: (assoc "Herman" telephone-directory :test #'equalp)
    
    Then use assoc to look up names using equal and using the default eql. Be sure you understand why each result occurs.

  2. What happens if you search by telephone-extension instead of by person? (For example, you might try
    (assoc 4202 telephone-directory).)

  3. Add entries to your birthday list, so that two people have the same names. Thus, there might be entries (Adams "October 30, 1735") and (Adams "July 11, 1767") for presidents John Adams and John Quincy Adams, respectively. What happens if you try to retrieve a pair with assoc using this common key? (For example, you might try
    (assoc 'Adams birthdays)).


This document is available on the World Wide Web as

http://www.math.grin.edu/~walker/courses/261/lab-beginning-LISP-3.html

created January 25, 1998
last revised January 25, 1998