Laboratory Exercises For Computer Science 261

More LISP

More LISP

Goals: This laboratory exercise continues an introduction of LISP, considering lists, Boolean values, conditional statements (cond and if), and recursion in LISP.

Lists

In LISP, a list is written by enclosing the elements of the list in parentheses. Here are some simple examples:

   (+ x y)
   (this is a list)
   (sqrt x)
   ()
These lists have 3, 4, 2, and 0 components, respectively. The list () with 0 components also is called the empty list or the null list. In LISP, the null list may be represented as either () or NIL.

Any or all components of a list can, in turn, be lists:

   (* pi (expt r 2))
   (+ (* x x) (* 3 x) 2)
   (all x (if (human x) (mortal x)))
   ( () )
   ((())) 
   ((()())())
In reviewing the second of these examples, (+ (* x x) (* 3 x) 2) is a list with four components:

   +
   (* x x)
   (* 3 x)
   2
List Extraction Functions: Parts of lists can be extracted by car and cdr: Examples:

   (car '(a b c))          =  a
   (car '((a) b c))        =  (a)
   (car (car '((a) b c)))  =  a
   (car 'a)     error: a is not a list.
   (car '())    error: () is not a pair.

   (cdr '(a b c))          =  (b c)
   (cdr '((a) b c))        =  (b c)
   (cdr (cdr '(a b c)))    =  (c)
   (cdr (car '((a) b c)))  =  ()
   (cdr 'a)     error: a is not a list.
   (cdr '())    error: () is not a pair.
In each of these examples, note that car and cdr are applied to a list which is introduced with a quote. Since parentheses are used both to group data into a list and to call procedures, the LISP interactive interface needs to see a single quote at the front of a list literal, so that it will treat it as a datum instead of evaluating it.

Since combinations of car and cdr are frequently used, all combinations up to four uses of car and cdr are defined as functions of the form cxxxr:


   (caar x)    = (car (car x))
   (cadr x)    = (car (cdr x))
   (caddr x)   = (car (cdr (cdr x)))
List Constructor Function:
Two basic functions that construct new list structures are the functions cons and list. If y is a list, then we can think of (cons x y) as adding the new element x to the front of the list y.

   (cons 'a '(b c))    =  (a b c)
   (cons 'a '())       =  (a)
   (cons '(a) '(b))    =  ((a) b)
The list function makes a list out of the elements that follow:

   (list 'a 'b 'c 'd)  = (a b c d)
   (list 'a '(b c))    =  (a (b c))
   (list 'a '())       = (a ())
   (list '(a) '(b))    =  ((a) (b))
List Manipulation Functions
append makes a new list consisting of the members of its argument lists. append takes any number of arguments.

   (append '(a) '(b))       =  (a b)
   (append '(a b) '(c d))   =  (a b c d)
   (append '(a) '(b) '(c))  =  (a b c)
reverse makes a new list that is the reverse of the top level of the list given as its argument.

   (reverse '(a b))         =  (b a)
   (reverse '((a b)(c d)))  =  ((c d)(a b))
length returns the number of components in the [top level] of a list.

   (length '(a))      =  1
   (length '(a b))    =  2
   (length '((a b)))  =  1
   (length '(+ (* x x) (* 3 x) 2)) = 4
Exercises:
  1. Type the following three expressions into LISP:
    
       (length 5)
       (length '(+ 2 3))
       (length (+ 2 3))
    
    In each case, explain the result.

  2. Consider the expression (all x (if (human x) (mortal x))). Write lisp expressions to extract each of the following:
    
       all
       if
       human
    
  3. Write a LISP expression that creates the list (a b c) out of the individual symbols a, b, and c, using the list function.

  4. Write a LISP expression that creates the list (a b c) out of the individual symbols a, b, and c and the null list NIL, using only the cons, car, and cdr functions.

  5. Explain what happens when you apply reverse to the lists:
    
       ((a) (b c))
       (+ (* x x) (* 3 x) 2)
    
  6. Evaluate the following slightly tricky forms:
    
    (append '(a b c) '( ))
    (list '(a b c) '( ))
    (cons '(a b c) '( ))
    
    In each case, explain why you received the result that you obtain.

Boolean Values and Expressions

LISP contains the Boolean values true (t) and false (NIL). Note that the same symbol is used for both the null list and for the Boolean value false. Also, note that LISP is reasonably liberal in its evaluation of expressions, in that anything that is not explicitly false (NIL) is considered true.

The LISP language includes the expressions "and", "or", and "not". When these are applied to Boolean values ( t and NIL), the results follow the conventions from mathematics. Thus, (and A B) is true if both A and B are true and false otherwise. (or A B) is true (t) if either A is true or B is true (or both). not applied to any true value produces false (NIL), and (not) applied to false (NIL) returns true (t).

More generally, "and" and "or" may take as many arguments as desired. "and" is true if all arguments are true, and "or" is true if any of the arguments is true.

Exercises:

  1. Evaluate the following expressions:
    
       (and t t)
       (and nil t)
       (and t nil)
       (and nil nil)
       (and t t t t)
       (and t t nil t)
       (or t t)
       (or nil t)
       (or t nil)
       (or nil nil)
       (or t t t t)
       (or nil t nil nil)
       (or nil nil nil nil)
       (not t)
       (not nil)
    
    In each case, explain why LISP produced the result given.

  2. Now, evaluate the following expressions:
    
       (and 'cat t)
       (and nil 'cat)
       (and 'cat nil)
       (and nil nil)
       (and t t 'cat t)
       (and t t t 'cat)
       (and t t t 'cat 'dog)
       (or t 'cat)
       (or 'cat t)
       (or nil 'cat)
       (or 'cat nil)
       (or 'cat 'dog)
       (or nil nil)
       (not 'cat)
       (not (not 'cat))
    
    In each case, state why you think LISP produces the result given. Generalize the results to indicate what values and, or, and not return when they are applied to non-Boolean values.

  3. Consider the following LISP procedure:
    
       (defun not-and (A B)
           (not (and A B)))
    
    Explain why this procedure evaluates the logical expression "Not (A And B))".
    Apply this procedure to various values (t, NIL) for A and B. In each case, be sure you understand why the machine prints the output that results.

  4. Write procedures to evaluate "(A And B) Or C" and "A And (B Or C)".
    Run these procedures with various values (t, NIL) for A, B, and C, and examine the output. In each case, be sure you can explain the resulting output.

Conditional Statements

As in other languages, LISP allows program execution to depend upon the value of Boolean expressions. Specifically, LISP contains two constructs that allow conditional execution: cond and if. As you will see, the cond statement is more general, and one could consider an if statement as just a special case of cond.

Cond Statements

LISP's cond statement uses expressions to determine what action is to be taken. This is illustrated in the following procedure:

    (defun type-of-number (A)
        (cond ((< A 0) "the number is negative")
              ((> A 0) "the number is positive")
              (t   "the number is zero")
        )
    )
Within this cond expression, (< A 0) is first examined. If it is true, the clause
"the number is negative" is evaluated and returned. [Evaluation of the cond expression is finished.] If, however, the expression (< A 0) is false, then evaluation proceeds with the next expression (> A 0). Again, if this expression is true, the following expression
"the number is positive" is evaluated, and evaluation of cond is completed. But if this expression also is false, then we move to the next part of cond. In LISP, the last condition for a cond statement always should be t. In this way, if we get this far, the action that follows always will be evaluated.

Exercises:

  1. Copy ~walker/261/labs/smallest.lsp to your account.
    Fill in the Boolean conditions (indicated as ??? in the file) to test for the cases specified.
    Test your procedure with several examples which cover each case.

If Statements

LISP's if expression uses a conditional expression to determine which of two actions to take. This is illustrated in the following procedure:

    (defun is-negative (A)
        (if (< A 0) 
                "the number is negative"
                "the number is nonnegative"
        )
    )
In executing the if expression, the condition (< A 0) is evaluated. If this is true, the next clause "the number is negative" is evaluated and returned. If the condition is not true, the final clause "the number is nonnegative" is evaluated and returned. Note that this construction is quite similar to an if-then-else statement in many languages, except that the keyword else is implicit, but not actually stated.

Exercises:

  1. Rewrite your procedure smallest, replacing the cond by if expressions. Thus, the main part of your procedure may have the form:
    
        (if (???1) 
            "A is smaller than both B or C." ;;then clause
            (;;; the else clause -- A is not smaller 
             if (???2) ;;second condition within cond
                 "B is smaller than both A and C."
                 ;;; the second else cause -- B is not smaller either
                 ??? etc.
            )
        )
    

Recursion in LISP

Much processing in LISP utilizes recursion; the body of a procedure includes one or more calls to the very same procedure -- calls that deal with simpler and more readily computable values of the parameters.

For instance, imagine that you want to define a procedure longest-on-list that 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"

If there is a tie for the longest string, let's say that whichever one appears first on the list wins:

(longest-on-list '("keep" "it" "short" "and" "sweet")) ===> "short"
(longest-on-list '("you" "can" "see" "the" "top")) ===> "you"

In starting to write this procedure, the first step is to determine which of two given strings is longer. This is accomplished easily utilizing LISP's built-in procedure length that computes the number of characters in a string:

(defun longer-string (str1 str2)
    (if (< (length str1) (length str2))
        str2
        str1))

With this helper fuction, we consider how to search a list containing an arbitrary number of strings. One simple case immediately comes to mind: When ls is a list that contains only one string, by default it is the longest string on the list, and we can simply extract it with car and return it. In any other case -- whenever the list is known to have two or more elements -- we can at least be confident that the longest string is either the first one on the list, or the longest string from the rest of the list. This suggests the following recursive procedure:

(defun longest-on-list (ls)
    (if (null (cdr ls))
        (car ls)
        (longer-string (car ls)
                       (longest-on-list (cdr ls)))))

While LISP's syntax may seem a bit different from other languages you may know, the approach here utilizes recursion in a rather common way.

Exercises:

  1. Load the above definitions of the longer-string and longest-on-list procedures into LISP, and confirm that it correctly finds the longest string in each of the six examples given at the beginning of this document.

  2. Try this procedure on the list ("red" "white" "and" "blue"), and explain how the procedure produces its result.

  3. Find out and account for what happens if longest-on-list is applied to an empty list.

Now consider the following procedure to compute the kth power of n:

(defun power (n k)
    (if (= k 1)
        n
        (* n (power n (- k 1)))))

  1. Give LISP this definition of power and confirm that it correctly computes the square of 12, the cube of 5, and the sixth power of 10.

  2. What happens if one tries to use this version of power to compute a negative power of a number? (You can use a "control-c" in LISP to halt execution, although you may need to be patient to see the effect of this command.)

  1. Write a recursive procedure countdown that takes any non-negative integer start as its argument returns a list of all the positive integers less than or equal to start, in descending order:

    (countdown 5) ===> (5 4 3 2 1)
    (countdown 1) ===> (1)
    (countdown 0) ===> ()
    

  2. Write a procedure replicate that takes two arguments, size and item, and returns a list of size elements, each of which is item:

    (replicate 6 'foo) ===> (foo foo foo foo foo foo)
    (replicate 2 NIL) ===> (NIL NIL)
    (replicate 1 15) ===> (15)
    (replicate 3 '(alpha beta)) ===> ((alpha beta) (alpha beta) (alpha beta))
    (replicate 0 'help) ===> ()
    

One very common pattern involves constructing a list by applying some given procedure to every element of a given list and collecting the results. For example, the following procedure constructs a list of the squares of the numbers on a given list:

(defun square-each-element (ls)
    (if (null ls)
        '() 
        (cons (* (car ls) (car ls))
              (square-each-element (cdr ls)))))
  1. Give a similar definition for a procedure double-each-element that takes a list of numbers and returns a list of their doubles:

    (double-each-element '(3 -62 41.4 17/4)) ===> (6 -124 82.8 17/2)
    (double-each-element '(0)) ===> (0)
    (double-each-element '()) ===> ()
    

In some cases, it may be helpful to define two procedures to solve a problem -- one to set up the initial call to the other, supplying additional arguments to assist the recursion. This is illustrated in the following problem and solution:

Problem : Write a procedure called tally-by-parity that takes any list ls of integers and returns a two-element list in which the first element is the number of odd integers in ls and the second is the number of even integers in ls.

(tally-by-parity '(2 3 5 7 11 13)) ===> (5 1)
(tally-by-parity '(0 1 2 3 4 5 6)) ===> (3 4)
(tally-by-parity '(-8 124 0 124)) ===> (0 4)
(tally-by-parity '()) ===> (0 0)
Proposed Solution:
(defun tally-by-parity (ls)
      (tally-helper ls '(0 0)))
(defun tally-helper (ls ct-pair)
      (cond ((null ls) ct-pair)
            ((oddp (car ls)) 
                   (tally-helper (cdr ls)
                         (list (+ 1 (car ct-pair)) (cadr ct-pair))))
            (t  (tally-helper (cdr ls) 
                         (list (car ct-pair) (+ 1 (cadr ct-pair)))))))
  1. Run this proposed solution on several examples, to check that it works.

  2. Explain in a few sentences how this solution works. Include a discussion of what procedure tally-helper does.

Use this idea of a helper procedure in solving the following:
  1. Write a procedure called iota that takes any non-negative integer upper-bound as argument and returns a list of the non-negative integers strictly less than upper-bound, in ascending order:

    (iota 6) ===> (0 1 2 3 4 5)
    (iota 2) ===> (0 1)
    (iota 1) ===> (0)
    (iota 0) ===> ()
    

This document is available on the World Wide Web as

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

created January 21, 1998
last revised January 22, 1998