Exam 2: Conditionals and Recursion


Assigned: Friday, 3 April 2009

Due: Beginning of class, Wednesday, 8 April

Preliminaries

Exam format

This is a take-home examination. You may use any time or times you deem appropriate to complete the exam, provided you return it to me by the due date.

There are 10 problems, each worth 10 points for a total of 100 points. Although each problem is worth the same amount, problems are not necessarily of equal difficulty.

Read the entire exam before you begin.

I expect that someone who has mastered the material and works at a moderate rate should have little trouble completing the exam in a reasonable amount of time. In particular, this exam is likely to take you about two to three hours, depending on how well you've learned the topics and how fast you work. You should not work more than four hours on this exam. Stop at four hours and write “There's more to life than CS” and you will earn at least 75 points on this exam.

I would also appreciate it if you would write down the amount of time each problem takes. Each person who does so will earn two points of extra credit. Since I worry about the amount of time my exams take, I will give two points of extra credit to the first two people who honestly report that they've spent at least three hours on the exam or completed the exam. (At that point, I may then change the exam.)

Academic honesty

This examination is open book, open notes, open mind, open computer, open Web. However, it is closed person. That means you should not talk to other people about the exam. Other than as restricted by that limitation, you should feel free to use all reasonable resources available to you.

As always, you are expected to turn in your own work. If you find ideas in a book or on the Web, be sure to cite them appropriately. If you use code that you wrote for a previous lab or homework, cite that lab or homework and the other members of your group.

Although you may use the Web for this exam, you may not post your answers to this examination on the Web. And, in case it's not clear, you may not ask others (in person, via email, via IM, by posting a please help message, or in any other way) to put answers on the Web.

Because different students may be taking the exam at different times, you are not permitted to discuss the exam with anyone until after I have returned it. If you must say something about the exam, you are allowed to say “This is among the hardest exams I have ever taken. If you don't start it early, you will have no chance of finishing the exam.” You may also summarize these policies. You may not tell other students which problems you've finished. You may not tell other students how long you've spent on the exam.

You must include both of the following statements on the cover sheet of the examination.

  1. I have neither received nor given inappropriate assistance on this examination.
  2. I am not aware of any other students who have given or received inappropriate assistance on this examination.

Please sign and date each statement. Note that the statements must be true; if you are unable to sign either statement, please talk to me at your earliest convenience. You need not reveal the particulars of the dishonesty, simply that it happened. Note also that inappropriate assistance is assistance from (or to) anyone other than Professor Weinman (that's me) or Professor Davis.

Presenting Your Work

You must present your exam to me in two forms: both physically and electronically. That is, you must write all of your answers using the computer, print them out, number the pages, put your name on the top of every page, and hand me the printed copy. You must also email me a copy of your exam. You should create the emailed version by copying the various parts of your exam and pasting them into an email message. In both cases, you should put your answers in the same order as the problems. Failure to name and number the printed pages will lead to a penalty of two points. Failure to turn in both versions may lead to a much worse penalty.

In many problems, I ask you to write code. Unless I specify otherwise in a problem, you should write working code and include examples that show that you've tested the code. Do not include images; I should be able to regenerate those.

Unless I explicitly ask you to document your procedures, you need not write introductory comments.

Just as you should be careful and precise when you write code and documentation, so should you be careful and precise when you write prose. Please check your spelling and grammar. Since I should be equally careful, the whole class will receive one point of extra credit for each error in spelling or grammar you identify on this exam. I will limit that form of extra credit to five points.

I will give partial credit for partially correct answers. I am best able to give such partial credit if you include a clear set of work that shows how you derived your answer. You ensure the best possible grade for yourself by clearly indicating what part of your answer is work and what part is your final answer.

Getting Help

I may not be available at the time you take the exam. If you feel that a question is badly worded or impossible to answer, note the problem you have observed and attempt to reword the question in such a way that it is answerable. Feel free to try to call me in the office (269-9812).

I will also reserve time at the start of classes next week to discuss any general questions you have on the exam.

Problems

Part A: Recursive procedures

Problem 1: Implementing map

Topics: Lists, recursion, higher-order procedures.

Before you learned about recursion over lists, you learned about the (map proc lst) procedure. Observe that map can be implemented recursively.

Implement (my-map proc lst), which produces a new list containing the results of applying proc to each value in lst. You should not use the built-in map procedure. Instead, use recursion.

Problem 2: Generalizing all and any

Topics: Boolean values, recursive procedures, predicates

The reading on list recursion, revisited discusses patterns for writing recursive procedures that test whether all values, or any value, in a list satisfy some predicate. We can implement each pattern directly as a higher-order procedure: that is, a procedure that takes another procedure as a parameter.

(a) Using the given template for all-___? as a guide, write a procedure (all? pred? lst) that returns #t if pred? is true for every value in lst, and #f otherwise.

(b) Using the given template for any-___? as a guide, write a procedure (any? pred? lst) that returns #t if pred? is true for at least one value in lst, and #f otherwise.

Part B: Decoding procedures

Topics: Code reading, testing, recursion, documentation, verifying preconditions, husk and kernel

As you've probably noted, we like to write long procedure names that make it perfectly clear what procedures are supposed to do. These procedures have names like image-compute-pixels! or rgb-complement. We also try to choose good names for the variables and parameters in our code.

However, some programmers prefer to give their procedures and variables short names, such as f and x. Some pay little attention to formatting.

Unfortunately, such short names can obfuscate code, particularly when combined with a lack of care in formatting. For example, consider the following useful procedure.

(define a (lambda (f s) (if (null? f) s (cons (car f) (a (cdr f) s)))))

Problem 3: Making the procedure readable

Make this procedure easier to read as follows.

First, reformat the procedure so that the indentation properly demonstrates nesting. That is, you should choose appropriate points to put in carriage returns. Remember that MediaScheme will re-indent for you after you put in those returns, as long as you select the text and type control-tab.

Then, figure out what the procedure does. After doing so, change the names in the procedure to clarify the roles of various things. You should certainly rename the procedure a and the parameters f and s. (As a hint, start by figuring out what types f and s should be.)

Problem 4: Documenting the mystery procedure

Write introductory comments (the six P's) to explain the purpose (and the other P's) of the procedure formerly named a. Pay particular attention to the preconditions.

Problem 5: Checking preconditions

Modify the procedure so that it uses husk-and-kernel style and checks whether its preconditions are met. Throw a separate error for each failed precondition.

Part C: Keeping secrets

A substitution cipher is a code that simply replaces one character with another in a one-to-one fashion. This is the kind of code that must be broken when solving the typical newspaper puzzle called a "cryptogram." A special case of this code is called a Caesar cipher, named for the Roman emperor Julius Caesar, who used it to communicate with his generals. A Caesar cipher simply shifts each character to another a certain number of places down the alphabet. In a Caesar cipher with a shift of 3, the letter A (character 0) would become D (character 3), P (character 15) would become S (character 18), and Y (character 24) would wrap-around to the front of the alphabet to become B (character 27, or, actually character 1).

With 26 characters in the Roman alphabet we use, there is a special Caesar cipher whose encryption algorithm is the same as the decryption algorithm. Since the Caesar cipher shifts things down the alphabet with wrap-around, if the shift is by 13 places, then it will also take another 13 place shift to "undo" the encryption. This is called a "ROT13" encryption, which is short for a "rotation" (or shift) by 13 places.

In our implementation, we will keep to capital letters A-Z only. We should first realize that characters in Scheme have a collating-sequence number. Fortunately, the collating-sequence numbers of A-Z are consecutive. However, they do not start at 0, which we will probably desire.

Problem 6: Transforming Characters

Topics: Characters, strings, map, composition, numeric operations

(a) Write a procedure (shift-down x) that takes as its parameter the collating-sequence number of a capital letter (A-Z), and returns an integer in the canonical range 0-25.

> (shift-down (char->integer #\A))
0
> (shift-down 65)
0
> (shift-down (char->integer #\P))
15
> (shift-down 80)
15
> (shift-down (char->integer #\Z))
25
> (shift-down 90)
25

(b) Write a procedure (add-13 x) that takes an integer as its argument and simply adds thirteen to it.

(c) Write a procedure (wrap-26 x) that takes as its argument a non-negative integer and produces a new number that results from "wrapping around" at 26. In other words, a number no larger than 25 should be produced (corresponding to the letter Z) . The idea is to produce the "rotation" effect of the 26 characters of the alphabet.

> (wrap-26 0)
0
> (wrap-26 11)
11
> (wrap-26 25)
25
> (wrap-26 26)
0
> (wrap-26 27)
1
> (wrap-26 38)
12
> (wrap-26 (+ 25 13))
12

(d) Write a procedure (shift-up x) that takes as its parameter an integer in the canonical range 0-25 and returns the corresponding the collating-sequence number of a capital letter (A-Z).

> (integer->char (shift-up 0))
#\A
> (shift-up 0)
65
> (integer->char (shift-up 15))
#\P
> (shift-up 15)
80
> (integer->char (shift-up 25))
#\Z
> (shift-up 25)
90

(e) Using the procedures from parts (a)-(d), define another procedure called char-rot13 that takes a character, and produces the ROT13-encoded character. You should make your definition as concise as possible.

> (char-rot13 #\A)
#\N
> (char-rot13 #\T)
#\G
> (char-rot13 (char-rot13 #\A))
#\A

Problem 7: Transforming Strings

Topics: Characters, strings, map

Write a procedure (str-rot13 str) that takes a string containing only capital letters as a parameter and returns the ROT13 version of the string. You should make your code as concise as possible.

> (str-rot13 "HELLO")
"URYYB"
> (str-rot13 (str-rot13 "HELLO"))
"HELLO"

Problem 8: Hidden Images

Topics: Filters, conditionals, RGB colors as integers.

In this problem, we consider another approach to keeping messages secret. Steganography is the process of hiding a message in an image such that no one, other than the sender and receiver, even knows there is any hidden information. One simple approach is to hide a black-and-white image inside of a full-color image. No one will ever notice if a photo has been changed by only 1/255 from the original brightness values.

Here is the algorithm we used to create our steganographic image.

  • Take the original or cover image, which is in full color, and also a secret image, in black and white. These images must have the same width and height.
  • For each row and column,
    • Take the color from the original image. If it is represented as an odd integer, subtract 1 so that the result is even.
    • If the color in the secret image is white, add 1 to the previous result. If it is black, add 0 to the previous result.
    • The resulting number is used as the color of this pixel in the new steganographic image.
  • Return the steganographic image.

(a) Write a procedure, (image-steg-decode steg-image), that takes a steganographic image produced using this algorithm and produces the secret image.

Hints: Use image-variant in your procedure. Observe that if the color is represented by an odd number, this corresponds to a white pixel in the secret image. If the color is represented by an even number, this corresponds to a black pixel in the secret image.

(b) Download this image of a cute little kitty (Professor Davis's cat, Sprite, when he was a kitten), and test your procedure on it. Describe the secret image hidden inside it.

Part D: Drawing Circles

Problem 9: Drawing Multiple Circles

Suppose we want to draw a background and three circles on canvas.

  • The background has randomly placed white dots.
  • The first circle has radius 32, is centered at (96,80), and is colored with a function that uses sine and tangent to produce horizontal bands.
  • The second has radius 16, is centered at (64,96), and is black.
  • The third has radius 45, is centered at (32,128), and is colored with shades of grey based on the distance from (4,128).

We might write

(define color-white (rgb-new 255 255 255))
(define color-black (rgb-new 0 0 0))

(define canvas (image-new 128 128))
(image-compute-pixels! canvas
                       (lambda (col row)
                         (if (zero? (random 128))
                             color-white
                             color-black)))
(image-compute-pixels! canvas 
                       (lambda (col row)
                         (if (>= 1024 (+ (square (- col 96)) (square (- row 80))))
                             (rgb-new (+ 128 (* 255 (sin (* (/ pi 32) row)))) 
                                      0
                                      (+ 128 (* -255 (tan (* (/ pi 32) row)))))
                             rgb-transparent)))
(image-compute-pixels! canvas 
                       (lambda (col row)
                         (if (>= 512 (+ (square (- col 64)) (square (- row 96))))
                             color-black
                             rgb-transparent)))
(image-compute-pixels! canvas
                       (lambda (col row)
                         (if (>= 2048 (+ (square (- col 32)) (square (- row 128))))
                             (let ((c (* 4 (sqrt (+ (square (- col 4))
                                                    (square (- row 128)))))))
                               (rgb-new c c c))
                             rgb-transparent)))
(image-show canvas)

Rewrite this code so that, instead of using one call to image-new and three calls to image-compute-pixels!, it uses a single call to image-compute.

Hint: Use a cond to decide which, if any, of the circles each position belongs in.

Hint: Which circle appears in front, and which appears behind? Your code should produce approximately the same image as the code above (up to some randomness).

Problem 10: Drawing Even More Circles

Topics: Numeric recursion, GIMP tools

In a previous reading, we saw how to draw several parallel lines. In this problem, we will explore drawing several circles that differ regularly in location and size, giving a motion or depth effect. Two procedures will be helpful for our purposes. First, we revise one from our reading that draws a filled circle on a given image. Second, it will also be useful to distinguish our circles from one another, so we will make use of a procedure to convert a position (column and row) to an RGB color. These are given below.
;;; Procedure:
;;;   position->color
;;; Parameters:
;;;   col, an integer
;;;   row, an integer
;;; Purpose:
;;;   Compute a color based on col and row.
;;; Produces:
;;;   color, an RGB color
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   Different col/row combinations are likely to give different colors.
;;;   Nearby col/row combinations may give similar colors.
(define position->color
  (lambda (col row)
    (rgb-new (+ 128 (* 128 (sin (* pi row 0.0625))))
             (+ 128 (* 128 (cos (* pi col 0.0625))))
             (+ 128 (* 128 (sin (* pi (+ row col) 0.0625)))))))

;;; Procedure:
;;;   draw-filled-circle!
;;; Parameters:
;;;   image, an image
;;;   col, an integer
;;;   row, an integer
;;;   radius, an integer
;;; Purpose:
;;;   Draws a filled circle with the specified in the current color, centered at (col,row).
;;; Produces:
;;;   [Nothing; Called for the side effect]
;;; Preconditions:
;;;   0 <= col < (image-width image)
;;;   0 <= row < (image-height image)
;;;   0 < radius
;;; Postconditions:
;;;   The image now contains the specified circle.  (The circle may not be visible.)
(define draw-filled-circle!
  (lambda (image col row radius)
    (image-select-ellipse! image REPLACE
                           (- col radius) (- row radius)
                           (+ radius radius) (+ radius radius))
    (image-fill-selection! image)
    (image-select-nothing! image)))

Write a procedure draw-moving-circles! for the following 6-P documentation.
;;; Procedure:
;;;   draw-moving-circles!
;;; Parameters:
;;;   image, an image
;;;   n, an integer
;;;   col, an integer
;;;   row, an integer
;;;   radius, an integer
;;;   hoffset, an integer
;;;   voffset, an integer
;;;   roffset, an integer
;;; Purpose:
;;;   Draw at most n circles, with the first centered at (col,row) 
;;;   having the radius given, and each subsequent circle center offset 
;;;   by the appropriate multiple of hoffset and voffset and radius changed
;;;   by the appropriate multiple of roffset.
;;; Produces:
;;;   [Nothing; Called for the side effect.]
;;; Preconditions:
;;;   All circles can be drawn on the image.
;;; Postcondtions:
;;;   The image has been appropriately modified. Less than n circles may 
;;;   be drawn if roffset is negative. A circle centered at (col,row) has a 
;;;   color given by (position->color col row).
Here are some examples.
(define img (image-new 100 100))
(image-show img)
(draw-moving-circles! img 10 30 20 20 5 3 -2)
(define img (image-new 100 100))
(image-show img)
(draw-moving-circles! img 10 30 80 5 3 -3 2)
(define img (image-new 100 100))
(image-show img)
(draw-moving-circles! img 5 70 70 25 -10 -10 -8)

Some questions and answers

Here we will post answers to questions of general interest. Please check here before emailing your questions!

Problem 2

Question: Shouldn't (all? pred? lst) be (all-pred? lst)?
Answer: No. The procedure you are to write is called all?, and it takes two parameters, pred?, a predicate, and lst a list. The predicate is applied to every value in the list, and all? only returns true if the predicate is true for every value in the list.

Problem 6

Question: For problem 6 part (e) about char-rot-13, is the parameter only a capital letter or do we have to write the procedure for canonical letter as well? Thank you.
Answer: Your procedure need only function correctly for Scheme character values of capital letters (i.e., #\A, #\B, #\C, ... , #\X, #\Y, #\Z).

Errata

Here you will find errors of spelling, grammar, and design that students have noted. Remember, each error found corresponds to a point of extra credit for everyone. We usually limit such extra credit to five points. However, if we make an astoundingly large number of errors, then we will provide more extra credit.

  • "Caesar" was misspelled as "Caeasar" in Part C. [BHS and JMK, 1 pt]
  • "subtract" was misspelled as "substract" in Problem 8. [JMK, 1 pt]
  • Problem 9 had two "second" circles. The latter one is actually the third. [CF, 1 pt]
  • An example in Problem 6(d) was missing one right parenthesis. [MAM, 1 pt]
  • The link in Problem 10 should have been to the Geometric Art reading. [ALA, 1 pt]

Jerod Weinman