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.
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.
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.
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.
These are not submitted, but you should work through them to build your Lisp intuition.
The following simple examples may help you start thinking as a Lisp programmer.
What is the value of the following expressions? Try to work them out yourself, and verify your answers on the computer:
(car '(inky clyde blinky pinky))
(cons 'inky (cdr '(clyde blinky pinky)))
(car (car (cdr '(inky (blinky pinky) clyde))))
(cons (+ 1 2) (cdr '(+ 1 2)))
(mapcar #'(lambda (x) (/ x 2)) '(1 3 5 9))
(mapcar #'(lambda (x) (car x)) '((inky 3) (blinky 1) (clyde 33)))
(mapcar #'(lambda (x) (cdr x)) '((inky 3) (blinky 1) (clyde 33)))
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
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)
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
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.
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:
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:
For example, 1 + \log_2(n) = \log_2(2) + \log_2(n) = \log_2(2 n).
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.
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))
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.
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))
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.
Submit your code to the GradeScope assignment named, for example, "Lab 1". You can submit in one of two ways:
Please do not change the names of the starter files. Also:
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.