This homework has three types of problems:
Self Check: You are strongly encouraged to think about and work through these questions, but you will not submit answers to them.
Problems: You will turn in answers to these questions.
Pair Programming: This part involves writing Lisp code. You are required to work with a partner on it. You are welcome to choose your own partner, but I can also assist in matching pairs --- simply send me an email message and I will pair you with someone else also looking for a partner. Please do not let finding a partner go until the last minute.
We will be working on the Unix lab computers throughout the semester.
The only applications you will need for this assignment are terminal
and emacs
. To find these after logging in, click on the top icon --- a
purple spiral --- in the tool bar on the left edge of the screen, and
then type the name of the application you would like to find into the
search box. If you are not familiar with either of these, I can point
you to some useful resources.
I encourage you to work in the Unix lab whenever you like, but also keep
in mind that you can ssh
to our computers from anywhere on campus.
Their names are listed on the department's web page. I will also provide
basic instructions to install software on your own computers, but I you
will be responsible for ensuring that things are set up properly.
(Required) Mitchell, Chapter 3.
(As Needed) The Lisp Tutorial from the "Resources" web page, as needed for the programming questions.
(Optional) J. McCarthy, Recursive functions of symbolic expressions and their computation by machine, Comm. ACM 3,4 (1960) 184--195. You can find a link to this on the cs334 web site. The most relevant sections are 1, 2 and 4; you can also skim the other sections if you like.
Mitchell, Problem 3.1
For this problem, use the lisp interpreter on the Unix machines in the computer lab, or own your own computer. See instructions here to set up your own computer.
You can use the lisp interpreter interactively by running the
command clisp
, which will enter the lisp read-eval-print loop.
Simply type "\^D" (Control-D) or the expression (quit)
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 type "\^D" (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.
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
Evaluation of a Lisp expression can either terminate normally (and
return a value), terminate abnormally with an error, or run forever.
Some examples of expressions that terminate with an error are
(/ 3 0)
, division by 0; (car ’a)
, taking the car
of an atom;
and (+ 3 "a")
, adding a string to a number. The Lisp system
detects these errors, terminates evaluation, and prints a message to
the screen. Suppose that you work at a software company that builds
word processing software in Impure Lisp (It's been done: Emacs!).
Your boss wants to handle errors in Lisp programs without
terminating the computation, but doesn't know how, so your boss asks
you to ...
...implement a Lisp construct (error? E)
that detects whether
an expression E
will cause an error. More specifically, your
boss wants evaluation of (error? E)
to (1) halt with value
\mathit{true} if evaluation of \tt E would terminate in
error, and (2) halt with value \mathit{false} otherwise.
Explain why it is not possible to implement the error?
construct as part of the Lisp environment.
...implement a Lisp construct (guarded E)
that either executes
E
and returns its value, or if E
would halt with an error,
returns 0 without performing any side effects. This could be
used to try to evaluate E
and if an error would occur, just
use 0 instead. For example,
(+ (guarded E) E') ; just E' if E halts with an error; E+E' otherwise
will have the value of \tt E' if evaluation of \tt E would
halt in error, and the value of \tt E + E' otherwise. How
might you implement the guarded
construct? What difficulties
might you encounter? Notice that unlike (error? E)
, evaluation
of (guarded E)
does not need to halt if evaluation of \tt E
does not halt.
Mitchell, Problem 3.2
(Based on Mitchell, Problem 3.5)
This question asks you to think about garbage collection in Lisp and compare our definition of garbage in the text to one given in McCarthy's 1960 paper on Lisp. McCarthy's definition is written for Lisp specifically, while our definition is stated generally for any programming language. Answer the question by comparing the definitions as they apply to Lisp only. Here are the two definitions.
Garbage, our definition: At a given point in the execution of a program P a memory location m is garbage if no continued execution of P from this point accesses location m.
Garbage, McCarthy's definition: "Each register that is accessible to the program is accessible because it can be reached from one or more of the base registers by a chain of car and cdr operations. When the contents of a base register are changed, it may happen that the register to which the base register formerly pointed cannot be reached by a car-cdr chain from any base register. Such a register may be considered abandoned by the program because its contents can no longer be found by any possible program.''
a. If a memory location is garbage according to our definition, is it necessarily garbage according to McCarthy's definition? Explain why or why not.
b. If a location is garbage according to McCarthy's definition, is it garbage by our definition? Explain why or why not.
c. There are garbage collectors that collect everything that is garbage according to McCarthy's definition. Would it be possible to write a garbage collector to collect everything that is garbage according to our definition? Explain why or why not.
Before starting on the programming, you will need to clone your
git
repository for this assignment and set it up to share with
yout partner. The GitLab tutorial on the CS 334 Resources page
contains more details about using GitLab and git, but the basic
steps are the following. Any of the class staff can help you get
things set up if you have not used git
before or run into trouble.
You will be working with a partner. Only one of you needs to do the following before working on the Lisp code.
Go to https://evolene.cs.williams.edu. (This server is only available on campus.)
Log in with your CS Unix username and password. You need to do this step and accept the terms of service before you'll be able to clone any repos.
You should see a project repository named "cs334-s23-hw1-UNIX_ID".
Clone that repository to your directory. You will use a command like the following.
git clone https://evolene.cs.williams.edu/freund/cs334-s23-hw1-UNIX_ID.git
Be sure to clone with HTTPS, and not SSH.
Server certificate verification failed?
If you encounter an
error about "server certificate verification failed" when using
your own computer or the lab machines, first run the following command in a terminal
window: git config --global http.sslVerify false
.
The repository contains files in which you will write the following short programming exercises. To push your changes to your GitLab repository, you'll first need to commit any edits you've made:
git commit -m "description of changes" -a
and then push them:
git push
You will need to give your partner access to your hw1 repository. To do this, follow the directions in Section 6 of the GitLab tutorial on the class Resources web page. Your repository will now appear in your partner's account on evolene, and your partner can clone and push changes to it as well.
Once these steps are complete, your partner should log in to evolene and clone the repository in the way described above. Your partner will have also have a repository names "cs334-s23-hw1-PARTNER_UNIX_ID" --- you won't need to clone or modify this in any way. Our submission system will identify partners and figure out which repository was used. The other one will just be ignored.
The only other git command you will likely need is pull
, which
updates your local copy with whatever changes have been pushed to
GitLab since you last pulled.
Please complete the following questions in the appropriate
lisp
files. 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 homework via GradeScope by the beginning of class on the due date.
Submit your answers to the Gradescope assignment named, for example, "HW 1". It should:
You will be asked to resubmit homework not satisfying these requirements. Please select the pages for each question when you submit.
If this homework includes programming problems, submit your code to the Gradescope assignment named, for example, "HW 1 Programming". Also:
Autograding: For most programming questions, 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. If you encounter difficulties or unexpected results from the autograder, please let us know.