University of Virginia, Department of Computer Science
CS200: Computer Science, Spring 2002

Problem Set 2: Function Fractals - Selected Answers

Honor Code Reminder: If you are currently taking CS200, it is a violation of the course pledge to look at Problem Set answers and discussions from previous years. Please don't do it.

Question 1: Define a function that uses make-point, window-draw-point and window-draw-line to draw a simple picture of a boat. The primary point of this not particularly pointed problem, is to point out how to practice producing pictures by putting point procedures in proximity pointedly. (That is, there is nothing to turn in for this, but attempt the next question until you can do this.)


Alyssa P. Hacker drew this picture of The Endurance, the boat Sir Ernest Shackleton used to explore the Antarctic (most of you drew simpler boats!)

Question 2:
  1. Define a function, vertical-mid-line that can be passed to draw-curve-points so that (draw-curve-points vertical-mid-line 1000) produces a vertical line in the middle of the window.

  2. Define a function, vertical-line that takes one parameter and produces a function that produces a vertical line at that horizontal location. For example, (draw-curve-points (vertical-line 0.5) 1000) should produce a vertical line in the middle of the window and (draw-curve-points (vertical-line 0.2) 1000) should produce a vertical line near the left side of the window.

Here's Rachel Dada's answer:

(define (vertical-mid-line p)
   (make-point 0.5 p))

(define (vertical-line m)
   (lambda (p) (make-point m p)))
Question 3: Define a function that draws a smiley face. You will probably need to define a function similar to first-half, but that takes less than half of the curve. You shouldn't need to use any fancy geometry (e.g., sin or cos), but instead use functions to alter the center-circle curve we have already defined to make the smile, eyes and nose. If you don't like procedures yet, you can draw a frowny face instead. If you are having a hard time drawing the mouth, read ahead a bit in the problem set (but try to do this with your own code).

Grace Deng drew a sleepy smiley face:

(define (draw-smiley)
  (draw-curve-points (translate (shrink (first-half (rotate-ccw (flip-vertically unit-circle))) 0.15) 0.5 0.5) 1000)
  (draw-curve-points (translate (shrink unit-circle .25) 0.5 0.5) 1000)
  (draw-curve-points (translate (shrink unit-circle .05) 0.5 0.5) 1000)
  (draw-curve-points (translate (shrink (first-half (rotate-ccw unit-circle)) 0.05) 0.4 0.6) 1000)
  (draw-curve-points (translate (shrink (first-half (rotate-ccw unit-circle)) 0.05) 0.6 0.6) 1000))
Question 4: If you aren't quite so happy about procedures, you may want to make a smiley face with a smaller smile. You could do that by applying first-half twice to get a quarter-curve (this produces a lop-sided smile, but that's okay):
      (draw-curve-points 
       (translate (rotate-cw (first-half (first-half (shrink unit-circle .25)))) 0.5 0.5)
       1000)
  1. Define a function twice that composes a function with itself. You should be able to use twice like this: (define quarter-curve (twice first-half)) to get a function quarter-curve that produces the first quarter of a curve.
  2. Define a function n-times that takes a function and number n, and composes the function with itself n times. Think about a recursive definition of n-times: to apply a function 1 time, just apply the function; to apply the function n times, compose the function with the result of applying the funtion n - 1 times.

Here's Jacques Fournier's answer:

(define (twice f) (compose f f))

(define (n-times f n)
  (if (= n 1)
      f
      (compose f (n-times f (- n 1)))))
Question 5:

Define a new smiley drawing function, smiley-curve but this time make it a single curve. The same smiley face you drew in Question 3 should now appear when you do,

   (draw-curve-points smiley-curve 1000)
Some parts of your smiley face probably appear dotty (instead of as solid lines). Explain why some parts of your smiley face are more dotty than others.

Optional (bonus points - don't try this until you have finished the rest of the problem set). Define a better function for turning many curves into a single curve that does not have this problem.

Here's Jeff Taylor and Katie Winstanley's answer:

(define (smiley-curve t)
  ((connect-rigidly (connect-rigidly (connect-rigidly make-face make-eye1) make-eye2) make-smile) t))

(define (make-face t)
  ((translate (shrink unit-circle .5) 0.5 0.5) t))

(define (make-eye1 t)
  ((translate (shrink unit-circle .125) 0.75 0.75) t))

(define (make-eye2 t)
  ((translate (shrink unit-circle .125) 0.25 0.75) t))

(define (half-smile curve)
  (lambda (t)
    ((rotate-ccw (flip-vertically curve)) (/ t 2))))

(define (make-smile t)
  ((translate (half-smile (shrink unit-circle .375)) 0.5 0.5) t))
The reason some parts of the smiley face appear dotty, it that connect-rigidly uses up half the points on the first curve. If we use connect-rigidly multiple times to connect several curves, only half the points are available for each curve (even when one of the curves may be the result of connecting several other curves). For the code above, half the points are used for make-smile, and the other half for (connect-rigidly (connect-rigidly (connect-rigidly make-face make-eye1) make-eye2). Of that half, half (a quarter of the original points) are used for make-eye2 and the rest for (connect-rigidly (connect-rigidly make-face make-eye1). Hence, only one eigth of the total points are used to make-face.

Question 6: Draw some gosper curves using (show-connected-gosper level). Print out your graphics window. Note that if all the code you wrote earlier works correctly, there is no new code required for this.

Question 7:
  1. For the original implementation without using let, how many times is (unit-line t) evaluated for each point on the curve produced by (gosper-curve 1)?
  2. For the original implementation without using let, how many times is (unit-line t) evaluated for each point on the curve produced by (gosper-curve 5)?
  3. If all the curve transformation procedures are rewritten to use let and avoid evaluating (curve t) more than once in their bodies, how many times is (unit-line t) evaluated for each point on the curve produced by (gosper-curve 5)?
  4. You can use (time (show-connected-gosper 5)) to get an accurate timing of how long it takes to evaluate (show-connected-gosper 5). Do this with the original definitions for a few different gosper levels. Then, rewrite the curve transformation procedures to be more efficient. Compare the resulting times, and explain if they are consistent with your answers to the previous parts of this question.

This was a tricky one. One way to find out how many times unit-line is being called is to use trace (as in Lecture 5):

> (require-library "trace.ss")
> (trace unit-line)
(unit-line)
> (define g1 (gosper-curve 1))
> (g1 0.0)
|(unit-line 0.0)
|#
|(unit-line 0.0)
|#
|(unit-line 0.0)
|#
|(unit-line 0.0)
|#
#
> (g1 0.1)
|(unit-line 0.2)
|#
|(unit-line 0.2)
|#
|(unit-line 0.2)
|#
|(unit-line 0.2)
|#
#
> (g1 0.6)
|(unit-line 0.19999999999999996)
|#
|(unit-line 0.19999999999999996)
|#
|(unit-line 0.19999999999999996)
|#
|(unit-line 0.19999999999999996)
|#
|(unit-line 0.19999999999999996)
|#
|(unit-line 0.19999999999999996)
|#
|(unit-line 0.19999999999999996)
|#
|(unit-line 0.19999999999999996)
|#
#
So, unit-line is called four times for some t-values for (gosper-curve 1) and eight times for other t-values. Why is this the case?

We created (gosper-curve 1) by using ((n-times gosperize 1) unit-line)) which is the same as (gosperize unit-line). The body of gosperize is:

(define (gosperize curve)
  (let ((scaled-curve 
	 (scale-x-y curve (/ (sqrt 2) 2) (/ (sqrt 2) 2))))
    (connect-rigidly (rotate-around-origin scaled-curve 45)
		     (translate
		      (rotate-around-origin scaled-curve -45)
		      .5 .5))))
We can use the Scheme rules of evaluation to see what happens when (gosperize unit-line) is evaluated. By Evaluation Rule 3a, we evaluate all the sub-expressions (both are names that evaluate to procedures). By Evaluation Rule 3b, we apply the first sub-expression to the rest. The first sub-expression is gosperize, a compound procedure, so we use Application Rule 2. This binds unit-line to curve in the body of gosperize:
  (let ((scaled-curve 
	 (scale-x-y unit-line (/ (sqrt 2) 2) (/ (sqrt 2) 2))))
    (connect-rigidly (rotate-around-origin scaled-curve 45)
		     (translate
		      (rotate-around-origin scaled-curve -45)
		      .5 .5))))
The let is syntactic sugar for lambda:
((lambda (scaled-curve)
   (connect-rigidly (rotate-around-origin scaled-curve 45)
		    (translate
		     (rotate-around-origin scaled-curve -45)
		     .5 .5)))
 
To evaluate this, we use Evaluation Rule 3 and then Application Rule 2:
(connect-rigidly (rotate-around-origin (scale-x-y unit-line (/ (sqrt 2) 2) (/ (sqrt 2) 2)) 45)
		 (translate
		  (rotate-around-origin (scale-x-y unit-line (/ (sqrt 2) 2) (/ (sqrt 2) 2)) -45)
                  .5 .5)))
Again, we can use the evaluation rules to evaluate this compound expression:
((lambda (curve1 curve2)
   (lambda (t)                             ; body of connect-rigidly (Evaluation Rule 2)
     (if (< t (/ 1 2))
	 (curve1 (* 2 t))
	 (curve2 (- (* 2 t) 1))))
   

Using application rule 2:
(lambda (t)                             ; body of connect-rigidly (Evaluation Rule 2)
   (if (< t (/ 1 2))
       ((rotate-around-origin (scale-x-y unit-line (/ (sqrt 2) 2) (/ (sqrt 2) 2)) 45) (* 2 t))
       ((translate (rotate-around-origin 
		    (scale-x-y unit-line (/ (sqrt 2) 2) (/ (sqrt 2) 2)) -45) .5 .5)
	(- (* 2 t) 1))))
We want to know what happens when this is applied, so let's apply it to 0.0 and follow the evaluation rules:
((lambda (t)                             ; body of connect-rigidly (Evaluation Rule 2)
   (if (< t (/ 1 2))
       ((rotate-around-origin (scale-x-y unit-line (/ (sqrt 2) 2) (/ (sqrt 2) 2)) 45) (* 2 t))
       ((translate (rotate-around-origin 
		    (scale-x-y unit-line (/ (sqrt 2) 2) (/ (sqrt 2) 2)) -45) .5 .5)
	(- (* 2 t) 1))))
 0.0)
By Application Rule 2:
(if (< 0.0 (/ 1 2))
    ((rotate-around-origin (scale-x-y unit-line (/ (sqrt 2) 2) (/ (sqrt 2) 2)) 45) (* 2 0.0))
    ((translate (rotate-around-origin 
		 (scale-x-y unit-line (/ (sqrt 2) 2) (/ (sqrt 2) 2)) -45) .5 .5)
     (- (* 2 0.0) 1)))
The Evaluation rule for the if special form says to evaluate the first expression (in this case, we get #t since 0.0 is less than 1/2) and then evaluate to the value of the second expression if the result was not false. So, this evaluates to:
((rotate-around-origin (scale-x-y unit-line (/ (sqrt 2) 2) (/ (sqrt 2) 2)) 45) (* 2 0.0))
Okay, we're making progress! We use Evaluation Rule 3, since it is an application. We need to evaluate the two subespressions: the second, (* 2 0.0) is easy, it evaluates to 0.0. The first subexpression (rotate-around-origin (scale-x-y unit-line (/ (sqrt 2) 2) (/ (sqrt 2) 2)) 45) evaluates using Evaluation Rule 2:
(((lambda (curve theta)
    (let ((cth (cos (degrees-to-radians theta)))
	  (sth (sin (degrees-to-radians theta))))
      (lambda (t)
	(let ((x (x-of-point (curve t)))
	      (y (y-of-point (curve t))))
	  (make-point
	   (- (* cth x) (* sth y))
	   (+ (* sth x) (* cth y)))))))
  (scale-x-y unit-line (/ (sqrt 2) 2) (/ (sqrt 2) 2)) 45)
 0.0)
Using Application Rule 2:
   (let ((cth (cos (degrees-to-radians 45)))
	 (sth (sin (degrees-to-radians 45))))
     (lambda (t)
       (let ((x (x-of-point ((scale-x-y unit-line (/ (sqrt 2) 2) (/ (sqrt 2) 2)) t)))
	     (y (y-of-point ((scale-x-y unit-line (/ (sqrt 2) 2) (/ (sqrt 2) 2)) t))))
	 (make-point
	  (- (* cth x) (* sth y))
	  (+ (* sth x) (* cth y)))))))
We only care how many times unit-line is evaluated. Here, we see it is used twice the same way: ((scale-x-y unit-line (/ (sqrt 2) 2) (/ (sqrt 2) 2)) t) to get x and y. So, the total number of times unit-line is evaluated will be twice the number of times it is evaluated to evaluate ((scale-x-y unit-line (/ (sqrt 2) 2) (/ (sqrt 2) 2)) t). By Evaluation Rule 2 we replace scale-x-y:
(((lambda (curve x-scale y-scale)
    (lambda (t)
      (make-point (* x-scale (x-of-point (curve t)))
		  (* y-scale (y-of-point (curve t))))))
  unit-line (/ (sqrt 2) 2) (/ (sqrt 2) 2))
 0.0)
Application Rule 2:
((lambda (t)
   (make-point (* (/ (sqrt 2) 2) (x-of-point (unit-line t)))
	       (* (/ (sqrt 2) 2) (y-of-point (unit-line t)))))
 0.0)
We use Evaluation Rule 3 and Application Rule 2 again:
(make-point (* (/ (sqrt 2) 2) (x-of-point (unit-line 0.0)))
	    (* (/ (sqrt 2) 2) (y-of-point (unit-line 0.0))))
So, unit-line is evaluated twice to evaluate ((scale-x-y unit-line (/ (sqrt 2) 2) (/ (sqrt 2) 2)) t), which is evaluates twice to evaluate (rotate-around-origin (scale-x-y unit-line (/ (sqrt 2) 2) (/ (sqrt 2) 2)) 45). This is consistent with our trace result that unit-line is evaluated 4 times when we apply ((gosper-curve 1) 0.0).

When we apply ((gosper-curve 1) 0.6) we saw unit-line was evaluates 8 times. The reason is because the second branch of the if it taken in connect-rigidly: Using application rule 2:

(lambda (t)                             ; body of connect-rigidly (Evaluation Rule 2)
   (if (< t (/ 1 2))
       ((rotate-around-origin (scale-x-y unit-line (/ (sqrt 2) 2) (/ (sqrt 2) 2)) 45) (* 2 t))
       ((translate (rotate-around-origin 
		    (scale-x-y unit-line (/ (sqrt 2) 2) (/ (sqrt 2) 2)) -45) .5 .5)
	(- (* 2 t) 1))))
This has an extra application of translate:
(define (translate curve x y)
  (lambda (t)
    (make-point
     (+ x (x-of-point (curve t)))
     (+ y (y-of-point (curve t))))))
It applies curve twice. From the first branch of the if, we saw that each time curve is applied, it requires 4 evaluations of unit-line. So, applying it twice will require 8 evaluations.

We'd be here an awfully long time if we tried to analyze (gosper-curve 5) the same way! Using trace, we can see that ((gosper-curve 5) 0.0) evaluates unit-line 1024 times (= 210). This make sense --- each time we apply gosperize, it quadruples the number of applications of unit-line to evaluate the resulting curve at t value 0.0 since it applies rotate-around-origin and scale-x-y to the curve, each of which evaluate the curve twice. Quadrupling five times is (* 4 4 4 4 4) = 1024.

But what about ((gosper-curve 5) 1.0)? For the second branch of the if, each gosperize level octuples the number of applications of unit-line! So, we will have (* 8 8 8 8 8) = 32768 = 215 evaluations of unit-line. Applying ((gosper-curve 5) t) to t-values betwen 0.0 and 1.0 will evaluate unit-line some number between 1024 and 32768 times.

The total number of evaluations ot unit-line to draw (show-connected-gosper 5) is 62058496 (I found this by using state to add a counter to unit-line, something you won't see until PS5, so I didn't expect you to do this). Since we are drawing 1000 points, the average number of evaluations per point is 62058. This is surprising, since it is higher than the worst case of gosper-curve 5 we worked out above! I'll leave it to you to figure out why. (Hint: we aren't just evaluating (gosper-curve 5) in show-connected-gosper, what does squeeze-rectangular-portion do?)

> (time (show-connected-gosper 5))
cpu time: 362081 real time: 362081 gc time: 0
After using let to eliminate all the duplicate evaluations, we should be able to evaluate (gosper-curve 5) with only one evaluation of unit-line. If we redefine translate, scale-x-y and rotate-around-origin to use let we eliminate the duplicate evaluations:
(define (translate curve x y)
  (lambda (t)
    (let ((ct (curve t)))
      (make-point
       (+ x (x-of-point ct))
       (+ y (y-of-point ct))))))

(define (scale-x-y curve x-scale y-scale)
  (lambda (t)
    (let ((ct (curve t)))
      (make-point (* x-scale (x-of-point ct))
		  (* y-scale (y-of-point ct))))))

(define (rotate-around-origin curve theta)
  (let ((cth (cos (degrees-to-radians theta)))
        (sth (sin (degrees-to-radians theta))))
    (lambda (t)
      (let ((ct (curve t)))
	(let ((x (x-of-point ct))
	      (y (y-of-point ct)))
	  (make-point
	   (- (* cth x) (* sth y))
	   (+ (* sth x) (* cth y))))))))  
Evaluating show-connect-gosper evaluates unit-line 2000 times:
> (time (show-connected-gosper 5))
cpu time: 341 real time: 340 gc time: 0
2000
We're really only drawing 1000 points, though, so it is still doing twice as much work as necessary. The problem is draw-curve-connected used to show the Gosper curve. Figuring out how to make this only evaluate unit-line once is a bit trickier than just using let.

The CPU times reported by time are 362081 before changing the code, and 341 after. The ratio is 1061, which is actually much smaller than the ratio between the number of evaluations of unit-line:

> (exact->inexact (/ 62058496 2000))
31029.248
The reason is that for the 2000-point curve, we are spending a lot of time on things other than just evaluating unit-line (such as actually drawing the points on the window).


CS 655 University of Virginia
Department of Computer Science
CS 200: Computer Science
David Evans
evans@virginia.edu
Using these Materials