CS 334: HW 1

Instructions

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 a Slack 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.

Reading

  • (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.

Self Check

Cons Cell Representations

Mitchell, Problem 3.1

Lisp Programming

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. 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))

(quit)

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. It is important that the program ends with (quit) so that the lisp interpreter will exit and return you to the Unix shell. If your program contains an error (or you forget the (quit) expression), the lisp interpreter will print an error message and then 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.

  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
    

Problems

1. Detecting Errors (10 pts)

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 ...

  1. ...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.

  2. ...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.

2. Conditional Expressions in Lisp (20 pts)

Mitchell, Problem 3.2

3. Definition of Garbage (10 pts)

(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.

Pair Programming (40 pts)

Lisp

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-f20-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-f20-hw1-UNIX_ID.git
    

    Be sure to clone with HTTPS, and not SSH.

    (Note: If you encounter an error about "server certificate verification failed" when using your own computer, 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-f20-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.

  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 algorithms graphics AI midterm)) 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 AI MIDTERM))
    (I NEED AN XXXX BECAUSE I HAD A XXXX XXXX)
    
    * (censor '(I LIKE PROGRAMMING LANGUAGES MORE THAN GRAPHICS OR ALGORITHMS))
    (I LIKE PROGRAMMING LANGUAGES MORE THAN XXXX OR XXXX)
    

    Operations like this that must processes every element in a structure are typically 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.