CS 334: Lab 1: Lisp Programming

Overview

In this lab, you will get hands-on practice with Lisp programming. You'll start with some warm-up exercises to build your Lisp intuition, then work through six programming problems covering recursion, list manipulation, higher-order functions, and structured data.

You are required to work with a partner on the programming problems. You are welcome to choose your own partner, but the instructor can also assist in matching pairs --- simply send an email and you will be paired with someone else also looking for a partner. Please do not let finding a partner go until the last minute.

Getting Started

Creating a GitHub Account

You will need a GitHub account to access the files for the labs this semester. You may have already created such an account on your own or for other classes. That's fine -- you can just keep using that account. If you do not have an account, you can sign up for one here.

See our Git and GitHub Tutorial for more details on using git.

Setting Up Your Repository

You will be working with a partner. Only one of you needs to set up the repository before working on the Lisp code.

  • You will receive an email with an invitation link to the lab1 assignment on GitHub Classroom. Click the link to accept the assignment. This creates a private repository containing the starter code.

  • Clone that repository to your computer:

    git clone https://github.com/williams-cs/cs334-lab1-YOURNAME.git
    
  • The repository contains files in which you will write the following short programming exercises. To save your work, commit your changes:

    git commit -m "description of changes" -a
    

    and then push them to GitHub:

    git push
    
  • Add your partner as a collaborator on your repository so they can also clone and push to it. Go to your repository on GitHub, click Settings, then Collaborators, and add your partner's GitHub username. See the Git and GitHub Tutorial for details.

    Once your partner accepts the collaborator invitation, they should clone the same repository:

    git clone https://github.com/williams-cs/cs334-lab1-YOURNAME.git
    

The only other git command you will likely need is pull, which updates your local copy with whatever changes have been pushed to GitHub since you last pulled. Run git pull before you start working each session.

As above, see the Git and GitHub Tutorial for more details on using git.

Running Lisp

You can use the lisp interpreter interactively by running the command clisp, which will enter the lisp read-eval-print loop. Simply press Control-D or type the expression (quit) and hit return to exit back to the terminal.

However, I recommend putting your lisp code into a file and then running the interpreter on that file.

To run the program in the file "example.lisp", type

clisp < example.lisp

at the command line. The interpreter will read, evaluate, and print the result of expressions in the file, in order. For example, suppose "example.lisp" contains the following:

; square a number
(defun square (x) (* x x))

(square 4)
(square (square 3))

Evaluating this file produces the following output:

SQUARE
16
81
Bye.

It evaluates the function declaration for "square", evaluates the two expressions containing square, and then quits. If your program contains an error, the lisp interpreter will print an error message and then may wait for you to input an expression to evaluate. Just type in "(quit)" at that point to exit the interpreter, or press Control-D to return to the read-eval-print loop.

The dialect of lisp we use is similar to what is described in the book, with a few notable exceptions. See the Lisp notes page on the handouts website for a complete list of the Lisp operations that we have discussed. You should not need anything beyond what is listed there. Try using higher-order functions (ie, mapcar and apply) where possible.

Warm-Up Exercises

These are not submitted, but you should work through them to build your Lisp intuition.

Evaluating Expressions

The following simple examples may help you start thinking as a Lisp programmer.

  1. What is the value of the following expressions? Try to work them out yourself, and verify your answers on the computer:

    1. (car '(inky clyde blinky pinky))

    2. (cons 'inky (cdr '(clyde blinky pinky)))

    3. (car (car (cdr '(inky (blinky pinky) clyde))))

    4. (cons (+ 1 2) (cdr '(+ 1 2)))

    5. (mapcar #'(lambda (x) (/ x 2)) '(1 3 5 9))

    6. (mapcar #'(lambda (x) (car x)) '((inky 3) (blinky 1) (clyde 33)))

    7. (mapcar #'(lambda (x) (cdr x)) '((inky 3) (blinky 1) (clyde 33)))

  2. Write a function called "list-len" that returns the length of a list. Do not use the built-in "length" function in your solution.

    * (list-len (cons 1 (cons 2 (cons 3 (cons 4 nil)))))
    
    4
    * (list-len '(A B (C D)))
    
    3
    
  3. Write a function "double" that doubles every element in a list of numbers. Write this two different ways--- first use recursion over lists and then use mapcar.

    * (double '(1 2 3))
    (2 4 6)
    

Debugging Hints

Here are three hints to aid in debugging lisp code. You may wish to try these out before starting the main programming questions.

  • If you ever see .s in your output, your are likely not creating lists properly. Be sure to always start with nil and then cons new elements onto your lists:

    (cons 'A (cons 'B (cons 'C nil))) ==> (A B C)   ;; good list
    (cons 'A (cons 'B 'C) ==> (A B . C)             ;; bad list
    
  • You can print values by inserting print expressions into your functions. For example, in the following, Lisp will print out that paramater passed into fact on each call:

    (defun fact (n)
      (print n)
      (if (eq n 1) 1 (* n (fact (- n 1)))))
    

    Print returns the value printed, so you can actually embed print inside larger expressions. This version prints out the result of the recursive call before performing the multiplication:

    (defun fact (n)
      (if (eq n 1) 1 (* n (print (fact (- n 1))))))
    
  • An alternative is to use trace, which prints out the sequence of function calls and results produced. For example, this code;

    (defun fact (n) (if (eq n 1) 1 (* n (fact (- n 1)))))
    
    (trace fact)
    (fact 4)
    

    prints:

    1. Trace: (FACT '4)
    2. Trace: (FACT '3)
    3. Trace: (FACT '2)
    4. Trace: (FACT '1)
    4. Trace: FACT ==> 1
    3. Trace: FACT ==> 2
    2. Trace: FACT ==> 6
    1. Trace: FACT ==> 24
    24
    

Programming Problems

Please complete the following questions in the appropriate lisp files in your repository. Your code should be reasonably documented (comment lines start with ";"), and each file should contain test cases to demonstrate that it works.

  1. Recursive Definitions

    Not all recursive programs take the same amount of time to run. Consider, for instance, the following function that raises a number to a power:

    (defun power (base exp)
        (cond ((eq exp 1) base)
            (t (* base (power base (- exp 1))))))
    

    A call to (power base e) takes e-1 multiplication operations for any e \geq 1. You could prove this time bound by induction on e:

    Theorem: A call to (power b e), where e \geq 1 takes at most e-1 multiplications.

    • Base case: e = 1. (power b 1) returns b and performs no multiplications, and e-1 = 0.

    • Ind. hyp: For all k < e, (power b k) takes at most k-1 multiplications.

    • Prove for e: Since e is greater than 1, the "else" branch is taken, which calls (power b (- e 1)). The induction hypothesis shows that the recursive call uses (e-1)-1 = e-2 multiplications. The result is then multiplied by the base, yielding a total of e - 2 + 1 = e-1 multiplications.

    Multiplication operations are typically very slow relative to other math operations on a computer. Fortunately, there are other means of exponentiation that use fewer multiplications and lead to more efficient algorithms. Consider the following definition of exponentiation:

    \begin{array}{rcll} b^1 & = & b \\ b^e & = & {(b^{(e/2)})^2} & \mbox{if $e$ is even}\\ b^e & = & b * (b^{e-1}) & \mbox{if $e$ is odd} \end{array}

    Write a Lisp function fastexp to calculate b^e for any e \geq 1 according to these rules. You will find it easiest to first write a helper function to square an integer, and you may wish to use the library function (mod x y), which returns the integer remainder of x when divided by y.

    Show that the program you implemented is indeed faster than the original by determining a bound on the number of multiplication operations required to compute (fastexp base e). Prove that bound is correct by induction (as in the example proof above), and then compare it to the bound of e-1 from the first algorithm. Include this proof as a comment in your code. Multline comments are delineated with #| and |#, as in: #| \ldots |#.

    Hint

    For fastexp, it may be easiest to think about the number of multiplications required when exponent e is 2^k for some k. Determine the number of multiplies needed for exponents of this form and then use that to reason about an upper bound for the others.

    The following property of the \log function may be useful in your proof:

    \log_b(m) + \log_b(n) = \log_b(m n)

    For example, 1 + \log_2(n) = \log_2(2) + \log_2(n) = \log_2(2 n).

  2. Recursive list manipulation

    Write a function merge-list that takes two lists and joins them together into one large list by alternating elements from the original lists. If one list is longer, the extra part is appended onto the end of the merged list. The following examples demonstrate how to merge the lists together:

    * (merge-list '(1 2 3) nil)
    (1 2 3)
    
    * (merge-list nil '(1 2 3))
    (1 2 3)
    
    * (merge-list '(1 2 3) '(A B C))
    (1 A 2 B 3 C)
    
    * (merge-list '(1 2) '(A B C D))
    (1 A 2 B C D)
    
    * (merge-list '((1 2) (3 4)) '(A B))
    ((1 2) A (3 4) B)
    

    Before writing the function, you should start by identifying the base cases (there are more than one) and the recursive case.

  3. Reverse

    Write a function rev that takes one argument. If the argument is an atom it remains unchanged. Otherwise, the function returns the elements of the list in reverse order:

    * (rev nil)
    nil
    
    * (rev 'A)
    A
    
    * (rev '(A (B C) D))
    (D (B C) A)
    
    * (rev '((A B) (C D)))
    ((C D) (A B))
    
  4. Mapping functions

    Write a function censor-word that takes a word as an argument and returns either the word or XXXX if the word is a "bad" word:

    * (censor-word 'lisp)
    lisp
    
    * (censor-word 'midterm)
    XXXX
    

    The lisp expression (member word '(extension midterm python lisp java)) evaluates to true if word is in the given list.

    Use this function to write a censor function that replaces all the bad words in a sentence:

    * (censor '(I NEED AN EXTENSION BECAUSE I HAD A MIDTERM))
    (I NEED AN XXXX BECAUSE I HAD A XXXX)
    
    * (censor '(MY PET PYTHON LIKES TO DRINK JAVA))
    (MY PET XXXX LIKES TO DRINK XXXX)
    

    Operations like this that must processes every element in a structure should be written using mapping functions in a functional language like Lisp. In some ways, mapping functions are the functional programming equivalent of a "for loop", and they are now found in main-stream languages like Python, Ruby, and even Java. Use a map function in your definition of censor.

  5. Working with Structured Data

    This part works with the following database of students and grades:

    ;; Define a variable holding the data:
    * (defvar grades '((Riley (90.0 33.3))
                        (Jessie (100.0 85.0 97.0))
                        (Quinn (70.0 100.0))))
    

    First, write a function lookup that returns the grades for a specific student:

    * (lookup 'Riley grades)
    
    (90.0 33.3)
    

    It should return nil if no one matches.

    Now, write a function averages that returns the list of student average scores:

    * (averages grades)
    
    ((RILEY 61.65) (JESSIE 94.0) (QUINN 85.0))
    

    You may wish to write a helper function to process one student record (ie, write a function such that (student-avg '(Riley (90.0 33.3))) returns (RILEY 61.65), and possibly another helper to sum up a list of numbers). As with censor in the previous part, the function averages function is most elegently expressing via a mapping operation (rather than recursion).

    We will now sort the averages using one additional Lisp primitive: sort. Before doing that, we need a way to compare student averages. Write a method compare-students that takes two "student/average" lists and returns true if the first has a lower average and nil otherwise:

    * (compare-students '(RILEY 61.65) '(JESSIE 94.0))
    t
    
    * (compare-students '(JESSIE 94.0) '(RILEY 61.65))
    nil
    

    To tie it all together, you should now be able to write:

    (sort (averages grades) #'compare-students)
    

    to obtain

    ((RILEY 61.65) (QUINN 85.0) (JESSIE 94.0))
    
  6. Deep Reverse

    Write a function deep-rev that performs a "deep" reverse. Unlike rev, deep-rev not only reverses the elements in a list, but also deep-reverses every list inside that list.

    * (deep-rev 'A)
    A
    
    * (deep-rev nil)
    NIL
    
    * (deep-rev '(A (B C) D))
    (D (C B) A)
    
    * (deep-rev '(1 2 ((3 4) 5)))
    ((5 (4 3)) 2 1)
    

    I have defined deep-rev on atoms as I did with rev.

Submitting Your Work

Submit your code to the GradeScope assignment named, for example, "Lab 1". You can submit in one of two ways:

  • Upload files: Click "Upload" and select all of your source files, or
  • Link GitHub: Click "GitHub" and select your repository and branch.

Please do not change the names of the starter files. Also:

  • If you worked with a partner, only one of each pair needs to submit the code.
  • Indicate who your partner is when you submit. Specifically, after you upload your files, there will be an "Add Group Member" button on the right of the Gradescope webpage -- click that and add your partner.

Autograding: Gradescope will run an autograder on your code that performs some simple tests. Be sure to look at the autograder output to verify your code works as expected. We will run more extensive tests on your code after the deadline.