Fundamentals of Computer Science I: Media Computing (CS151.02 2007F)

Exam 2: Recursion


Distributed: Friday, 2 November 2007

Due: 10 a.m., Friday, 9 November 2007

Preliminaries

Exam format

There are four problems on the exam. Some problems have subproblems. Each problem is worth twenty-five (25) points. The point value associated with a problem does not necessarily correspond to the complexity of the problem or the time required to solve the problem.

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.

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 four to six hours, depending on how well you've learned topics and how fast you work. You should not work more than eight hours on this exam. Stop at eight hours and write “There's more to life than CS” and you will earn at least 80 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 five 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.

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 Rebelsky (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. You ensure the best possible grade for yourself by emphasizing your answer and including a clear set of work that you used to derive the 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. If it's a reasonable hour (before 10 p.m. and after 8 a.m.), feel free to try to call me in the office (269-4410) or at home (236-7445).

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

Problems

Problem 1: Explaining Procedures

Topics: Documentation, code reading, local procedures, 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 region.compute-pixels! or list.member?. We also try to choose good names for the local and shared variables we use in our code.

However, some programmers prefer to give their procedures and variables short names, such as f and x. Others also 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. It is difficult for most programmers to figure out what the procedure does and how it does what it does.

(define r (lambda (l) (letrec ((c (lambda (p v) (let ((x (if (odd? v)
1 0))) (cons (+ (car p) x) (+ (cdr p) (- 1 x)))))) (s (lambda (q m)
(if (null? m) (/ (car q) (cdr q)) (s (c q (car m)) (cdr m)))))) (s
(cons 0 0) l))))

The original version of this procedure (above) used a technique that had been introduced only indirectly. You may instead use the following version. (Both compute the same value.)

(define r (lambda (l) (letrec ((c (lambda (p v) (let ((x (if (odd? v)
1 0))) (list (+ (car p) x) (+ (cadr p) (- 1 x)))))) (s (lambda (q m)
(if (null? m) (/ (car q) (cadr q)) (s (c q (car m)) (cdr m)))))) (s
(list 0 0) l))))

a. Reformat the procedure so that the indentation properly demonstrates nesting. That is, you should choose appropriate points to put in (and remove) carriage returns. Remember that DrFu will re-indent for you after you put in those returns, as long as you select the text and hit tab.

b. Figure out what the procedure does. After doing so, change the various names in the procedure to clarify the roles of the various things. You should certainly rename r, l, c, p, v, s, q, and m.

c. While the names will help your successors read the procedure, they will also benefit from some explanation. Add internal comments to explain the various parts.

d. Add introductory comments (the six P's) to explain the purpose (and the other P's) of the procedure. Pay particular attention to the preconditions.

e. Modify the husk so that it checks whether the procedure's preconditions are met. Throw a separate error for each failed precondition.

Problem 2: Concentric Circles

Topics: Numeric recursion, Scheme drawing

In a recent laboratory, we explored applications of numeric recursion to drawing geometric figures. In that exploration, we began to think about (but did not conclude) ways to draw concentric circles. As we saw with techniques for drawing parallel lines, there are many ways you might parameterize a procedure that draws multiple figures.

One potentially interesting technique is to draw circles in a geometric progression, with each circle's radius some fraction of the previous circle's radius. When do we stop? We stop when the area of the circle is small enough that it's unlikely to look very good. Since moving centers adds complexity, we won't worry about that. Although changing colors makes the image more interesting, we also won't worry about colors.

Write a procedure, image.draw-concentric-circles!, that takes six parameters: image, the id of an image; col, the column in which the center appears; row, the row in which the center appears; initial-radius, the radius of the outermost circle; ratio, the ratio in radius between a circle and its predecessor; and minimum-area, the area of the smallest circle we want drawn.

For example, the call (image.draw-concentric-circles! canvas 80 90 100 0.6 75) would draw

  • a circle of radius 100 centered at (80,90),

  • a circle of radius 60 (0.6 * 100) centered at (80,90),

  • a circle of radius 36 (0.6 * 60) centered at (80,90),

  • a circle of radius 21.6 centered at (80,90),

  • a circle of radius 12.96 centered at (80,90), and

  • a circle of radius 7.776 centered at (80,90).

Why do we stop when we reach 7.776? Because the next step is 4.6656, and that number squared, times pi, is about 69, which is less than 75.

Problem 3: Representing Images as Lists of Shapes

Topics: Data representation, List recursion, Conditionals, map, foreach!, Precondition checking

We've now seen a large number of ways to algorithmically describe images.

  • We can describe them in terms of the color at each position, which we did with the image.get-pixel! and image.set-pixel! procedures.

  • We can describe them in terms of a function that determines the color of each pixel based on the position of each pixel, as we did with image.position-map!.

  • We can describe them as lists of positions and colors which we can then render in various sizes, as we did with spot lists.

  • We can describe them in terms of a sequence of operations used to draw them, as we did in our exploration of the GIMP drawing tools.

There are, of course, many other techniques possible. One common one is to represent images as sequences of simple geometric shapes, which are to be drawn in sequence. As you've already seen, one of the most convenient ways to represent any type of data in Scheme is with a list, so we will represent each shape as a list, and a sequence of shapes to be drawn as a list of lists. Each list that represents a shape will begin with a string that names the shape. For now, we'll use four kinds of shapes: squares, boxes, circles, and discs. These lists will have the following form.

  • ("square" color col row size) represents an empty square of the specified color, centered at (col,row), and with edge length size.

  • ("box" color col row size) represents a filled square of the specified color, centered at (col,row), and with edge length size.

  • ("circle" color col row size) represents an empty circle of the specified color, centered at (col,row), and with diameter size.

  • ("disc" color col row size) represents a filled circle of the specified color, centered at (col,row), and with diameter size.

You'll note that these were carefully designed so that each list is of length five, with the first element the kind of object, the second the color, the third the column, the fourth the row, and the fifth the size.

a. Write a procedure, (image.render-object! image object), that renders an object (square, box, circle, or disc, determined by checking the first element of the list) in the particular image. You may use the current brush in rendering an object. In writing this procedure, you need not verify that object is a valid object. That is, your procedure can fail miserably if the client makes object (a) something other than a list; (b) a list that doesn't begin with "square", "box", "circle", or "disk"; (c) a list with the correct starting value but an incompatible length; (d) a list of the appropriate length in which the parameters are not of the appropriate type; or (e) anything equally senseless. [10 points]

> (define canvas (image.new 200 200))
> (image.show canvas)
> (image.render-object! canvas (list "disc" color.red 100 100 150))
; Ooh!  Look at that nice red disc.
> (image.render-object! canvas (list "square" color.black 50 50 25))
; And now there's a small black square in the upper-left quadrant.
> (image.render-object! canvas (list "circle" color.yellow 0 0 50))
; And a partial circle in the corner.

b. Write a procedure, (image.render-objects! image object-list), that renders a list of objects. Again, you can assume that the parameters are correct. [5 points]

> (define rainbow-disc (list (list "disc" color.red 100 100 100)
                             (list "disc" color.orange 100 100 90)
                             (list "disc" color.yellow 100 100 80)
                             (list "disc" color.green 100 100 70)
                             (list "disc" color.blue 100 100 60)
                             (list "disc" color.indigo 100 100 50)
                             (list "disc" color.violet 100 100 40)))
> (image.render-objects! canvas rainbow-disc)

c. Write a procedure, (objects.scale object-list factor), that creates a new list of objects by scaling the size of each object in the list by factor. Once again, you can assume that the parameters are correct. [5 points]

> (objects.scale rainbow-disc 0.7)
(("disc" -16776961 100 100 70)
 ("disc" -8978177 100 100 63)
 ("disc" -65281 100 100 56)
 ("disc" 16711935 100 100 49)
 ("disc" 65535 100 100 42)
 ("disc" 1711341567 100 100 35)
 ("disc" 1328500735 100 100 28)))

d. Of course, most of these procedures should check to ensure that the parameters are correct, and throw an error if they are not. Since two of these procedures impose the same requirement, we might want a common mechanism for testing validity. Write a procedure, (check-objects! object-list), that ensures that object-list is a list of objects. If it is not, the procedure should throw an appropriate error. If it is, the procedure can return #t. [5 points]

Here are some examples of the kinds of errors check-objects! might report. These examples are not intended to be comprehensive.

> (check-objects! "circle")
Error: The parameter is not a list.
> (check-objects! (list "circle" color.red 10 10 50))
Error: One of the elements in the object list is not itself a list.
> (check-objects! (list (list "circle" color.red 10 10 50)))
#t
> (check-objects! (list (list "rectangle" color.blue 10 10 50)))
Error: One of the elements in the object list does not have a valid object type.
> (check-objects! (list (list "circle" color.red 10 10)))
Error: One of the elements in the object list has too few characteristics.
> (check-objects! (list (list "square" color.red 10 10 50 40)))
Error: One of the elements in the object list has too many characteristics.
> (check-objects! (list (list "square" "red" 10 10 50)))
Error: One of the elements in the object list has an invalid color.
> (check-objects! (list (list "disc" color.blue 10 10 0)))
Error: One of the elements in the object list has an invalid size.
> (check-objects! null)
#t
> (check-objects! (list null))
Error: One of the elements in the object list is an empty list.

Problem 4: Merging Spot Lists

Topics: List recursion with multiple lists, spot lists, randomness

One of the interesting image processing problems that many programmers face is how to combine two images. There are a variety of techniques possible. One can average each pixel, take pixels from one image only when the corresponding pixel from another image has a particular value, or use other functions. Of course, the way we combine images also has to do with how we represent the images.

As you may recall, one of the ways we represent images is with lists of spots, where each spot has a column, a row, and a color. We can render each spot as a pixel, or as a larger section of screen.

a. Write a procedure, (spots.find-by-pos spot-list col row), that finds a spot in spot-list whose column is col and whose row is row. If no such spot exists, spots.find-by-pos should return #f. [10 points]

> (spots.find-by-pos null 10 10)
#f
> (define some-spots (list (spot.new 10 10 color.black)
                           (spot.new 10 11 color.black)
                           (spot.new 10 12 color.black)))
> (spots.find-by-pos some-spots 10 11)
(10 11 255)
> (spots.find-by-pos some-spots 11 10)
#f

b. Write a procedure, (spots.remove spot-list spot). [5 points]

In writing this procedure, you may find it useful to rely on the legendary list.remove procedure, defined as follows.

;;; Procedure:
;;;   list.remove
;;; Parameters:
;;;   lst, a list
;;;   val, a value
;;; Purpose:
;;;   Remove all copies of val from lst.
;;; Produces:
;;;   newlst
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   val does not appear in newlst.
;;;   Each value that appears in lst appears the same number of times 
;;;    in newlst.
;;;   Each value that appears in newlst appears the same number of
;;;    times in lst.
;;;   These values appear in the same order in the two lists.
(define list.remove
  (lambda (lst val)
    (cond
      ((null? lst) 
       null)
      ((equal? val (car lst)) 
       (list.remove (cdr lst) val))
      (else 
       (cons (car lst) (list.remove (cdr lst) val))))))

c. Write a procedure, (merge-spot-lists lst1 lst2), that merges two lists of spots. The merge of two spot lists consists of the following:

  • All spots from the first list which do not have the same position as any spot in the second list.

  • All spots from the second list which do not have the same position as any spot in the first list.

  • A random choice between spots in the two lists that share the same position. That is, if the first list contains a spot at (10,5), and the second list also contains a spot at (10,5), then half the time you should end up with the first spot in the result list and the other half of the time you should end up with the second spot.

> (define more-spots (list (spot.new 8 10 color.white)
                           (spot.new 9 10 color.white)
                           (spot.new 10 10 color.white)))
> (define also-spots (list (spot.new 10 10 color.white)
                           (spot.new 10 11 color.white)
                           (spot.new 10 12 color.white)))
> (merge-spot-lists some-spots more-spots)
((10 10 255)
 (10 11 255)
 (10 12 255)
 (8 10 -1)
 (9 10 -1))
> (merge-spot-lists some-spots more-spots)
((10 10 -1)
 (10 11 255)
 (10 12 255)
 (8 10 -1)
 (9 10 -1))
> (merge-spot-lists some-spots null)
((10 10 255)
 (10 11 255)
 (10 12 255))
> (merge-spot-lists null some-spots)
((10 10 255)
 (10 11 255)
 (10 12 255))
> (merge-spot-lists some-spots also-spots)
((10 10 -1)
 (10 11 255)
 (10 12 255))
> (merge-spot-lists some-spots also-spots)
((10 10 255)
 (10 11 -1)
 (10 12 255))

You may assume that neither individual list contains spots with duplicate positions. You also need not preserve the order of spots. [10 points]

Hint! There's a reason we asked you to do parts a and b of this problem.

Hint! You'll need to recurse over at least one of the lists.

Some questions and answers

Problem 1

Problem 2

Problem 3

Problem 4

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. I usually limit such extra credit to five points. However, if I make an astoundingly large number of errors, then I will provide more extra credit.

  • color was missing from the examples of circle and disc objects in the introduction to Problem 3. [PS and others, 1 point] (2 points in Rebelsky's section)
  • The code in problem 1 used a technique in Scheme that we've only introduced indirectly. [HS, 1 point]
  • Missing the word “if” in “... your procedure can fail miserably the client ...”. [JL and Alex M., 1 point]
  • Wrote image.render.object rather than image.render-object in one place. [YX and PS, 1 point]
  • Used check-objects rather than check-objects! in a few places. [YX, 1 point]

Five points reached (six in Rebelsky's). Each five additional errors yield another point.

  • Wrote “a circle of radius 36 (0.6 * 100)”; should have been “a circle of radius 36 (0.6 * 60)”. [PB]
  • The example run for problem 3(c) should show colors as integers rather than variable names. [PB]
  • The example runs for problem 4(c) should not include spots with the same position. [TB]
  • In problem 3d, the example “(check-objects! (list (list "rectangle" color.blue 10 10 50 60)))” has an incorrect length in addition to an invalid object type. [PB]

Creative Commons License

Samuel A. Rebelsky, rebelsky@grinnell.edu

Copyright 2007 Janet Davis, Matthew Kluber, and Samuel A. Rebelsky. (Selected materials copyright by John David Stone and Henry Walker and used by permission.)

This material is based upon work partially supported by the National Science Foundation under Grant No. CCLI-0633090. Any opinions, findings, and conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the National Science Foundation.

This work is licensed under a Creative Commons Attribution-NonCommercial 2.5 License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/2.5/ or send a letter to Creative Commons, 543 Howard Street, 5th Floor, San Francisco, California, 94105, USA.