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 makepoint, windowdrawpoint and windowdrawline 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:
 Define a function, verticalmidline that can be passed to drawcurvepoints so that (drawcurvepoints verticalmidline 1000) produces a vertical line in the middle of the window.
 Define a function, verticalline that takes one parameter and produces a function that produces a vertical line at that horizontal location. For example, (drawcurvepoints (verticalline 0.5) 1000) should produce a vertical line in the middle of the window and (drawcurvepoints (verticalline 0.2) 1000) should produce a vertical line near the left side of the window.
Here's Rachel Dada's answer:
(define (verticalmidline p) (makepoint 0.5 p)) (define (verticalline m) (lambda (p) (makepoint m p)))
Question 3: Define a function that draws a smiley face. You will probably need to define a function similar to firsthalf, 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 centercircle 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 (drawsmiley) (drawcurvepoints (translate (shrink (firsthalf (rotateccw (flipvertically unitcircle))) 0.15) 0.5 0.5) 1000) (drawcurvepoints (translate (shrink unitcircle .25) 0.5 0.5) 1000) (drawcurvepoints (translate (shrink unitcircle .05) 0.5 0.5) 1000) (drawcurvepoints (translate (shrink (firsthalf (rotateccw unitcircle)) 0.05) 0.4 0.6) 1000) (drawcurvepoints (translate (shrink (firsthalf (rotateccw unitcircle)) 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 firsthalf twice to get a quartercurve (this produces a lopsided smile, but that's okay): (drawcurvepoints (translate (rotatecw (firsthalf (firsthalf (shrink unitcircle .25)))) 0.5 0.5) 1000)
 Define a function twice that composes a function with itself. You should be able to use twice like this: (define quartercurve (twice firsthalf)) to get a function quartercurve that produces the first quarter of a curve.
 Define a function ntimes that takes a function and number n, and composes the function with itself n times. Think about a recursive definition of ntimes: 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 (ntimes f n) (if (= n 1) f (compose f (ntimes f ( n 1)))))
Question 5: Define a new smiley drawing function, smileycurve but this time make it a single curve. The same smiley face you drew in Question 3 should now appear when you do,
(drawcurvepoints smileycurve 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:
The reason some parts of the smiley face appear dotty, it that connectrigidly uses up half the points on the first curve. If we use connectrigidly 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 makesmile, and the other half for (connectrigidly (connectrigidly (connectrigidly makeface makeeye1) makeeye2). Of that half, half (a quarter of the original points) are used for makeeye2 and the rest for (connectrigidly (connectrigidly makeface makeeye1). Hence, only one eigth of the total points are used to makeface.(define (smileycurve t) ((connectrigidly (connectrigidly (connectrigidly makeface makeeye1) makeeye2) makesmile) t)) (define (makeface t) ((translate (shrink unitcircle .5) 0.5 0.5) t)) (define (makeeye1 t) ((translate (shrink unitcircle .125) 0.75 0.75) t)) (define (makeeye2 t) ((translate (shrink unitcircle .125) 0.25 0.75) t)) (define (halfsmile curve) (lambda (t) ((rotateccw (flipvertically curve)) (/ t 2)))) (define (makesmile t) ((translate (halfsmile (shrink unitcircle .375)) 0.5 0.5) t))
Question 6: Draw some gosper curves using (showconnectedgosper 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:
 For the original implementation without using let, how many times is (unitline t) evaluated for each point on the curve produced by (gospercurve 1)?
 For the original implementation without using let, how many times is (unitline t) evaluated for each point on the curve produced by (gospercurve 5)?
 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 (unitline t) evaluated for each point on the curve produced by (gospercurve 5)?
 You can use (time (showconnectedgosper 5)) to get an accurate timing of how long it takes to evaluate (showconnectedgosper 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 unitline is being called is to use trace (as in Lecture 5):
So, unitline is called four times for some tvalues for (gospercurve 1) and eight times for other tvalues. Why is this the case?> (requirelibrary "trace.ss") > (trace unitline) (unitline) > (define g1 (gospercurve 1)) > (g1 0.0) (unitline 0.0) #(unitline 0.0) # (unitline 0.0) # (unitline 0.0) # # > (g1 0.1) (unitline 0.2) # (unitline 0.2) # (unitline 0.2) # (unitline 0.2) # # > (g1 0.6) (unitline 0.19999999999999996) # (unitline 0.19999999999999996) # (unitline 0.19999999999999996) # (unitline 0.19999999999999996) # (unitline 0.19999999999999996) # (unitline 0.19999999999999996) # (unitline 0.19999999999999996) # (unitline 0.19999999999999996) # # We created (gospercurve 1) by using ((ntimes gosperize 1) unitline)) which is the same as (gosperize unitline). The body of gosperize is:
(define (gosperize curve) (let ((scaledcurve (scalexy curve (/ (sqrt 2) 2) (/ (sqrt 2) 2)))) (connectrigidly (rotatearoundorigin scaledcurve 45) (translate (rotatearoundorigin scaledcurve 45) .5 .5))))We can use the Scheme rules of evaluation to see what happens when (gosperize unitline) is evaluated. By Evaluation Rule 3a, we evaluate all the subexpressions (both are names that evaluate to procedures). By Evaluation Rule 3b, we apply the first subexpression to the rest. The first subexpression is gosperize, a compound procedure, so we use Application Rule 2. This binds unitline to curve in the body of gosperize:(let ((scaledcurve (scalexy unitline (/ (sqrt 2) 2) (/ (sqrt 2) 2)))) (connectrigidly (rotatearoundorigin scaledcurve 45) (translate (rotatearoundorigin scaledcurve 45) .5 .5))))The let is syntactic sugar for lambda:((lambda (scaledcurve) (connectrigidly (rotatearoundorigin scaledcurve 45) (translate (rotatearoundorigin scaledcurve 45) .5 .5)))To evaluate this, we use Evaluation Rule 3 and then Application Rule 2:(connectrigidly (rotatearoundorigin (scalexy unitline (/ (sqrt 2) 2) (/ (sqrt 2) 2)) 45) (translate (rotatearoundorigin (scalexy unitline (/ (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 connectrigidly (Evaluation Rule 2) (if (< t (/ 1 2)) (curve1 (* 2 t)) (curve2 ( (* 2 t) 1))))Using application rule 2:(lambda (t) ; body of connectrigidly (Evaluation Rule 2) (if (< t (/ 1 2)) ((rotatearoundorigin (scalexy unitline (/ (sqrt 2) 2) (/ (sqrt 2) 2)) 45) (* 2 t)) ((translate (rotatearoundorigin (scalexy unitline (/ (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 connectrigidly (Evaluation Rule 2) (if (< t (/ 1 2)) ((rotatearoundorigin (scalexy unitline (/ (sqrt 2) 2) (/ (sqrt 2) 2)) 45) (* 2 t)) ((translate (rotatearoundorigin (scalexy unitline (/ (sqrt 2) 2) (/ (sqrt 2) 2)) 45) .5 .5) ( (* 2 t) 1)))) 0.0)By Application Rule 2:(if (< 0.0 (/ 1 2)) ((rotatearoundorigin (scalexy unitline (/ (sqrt 2) 2) (/ (sqrt 2) 2)) 45) (* 2 0.0)) ((translate (rotatearoundorigin (scalexy unitline (/ (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:((rotatearoundorigin (scalexy unitline (/ (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 (rotatearoundorigin (scalexy unitline (/ (sqrt 2) 2) (/ (sqrt 2) 2)) 45) evaluates using Evaluation Rule 2:(((lambda (curve theta) (let ((cth (cos (degreestoradians theta))) (sth (sin (degreestoradians theta)))) (lambda (t) (let ((x (xofpoint (curve t))) (y (yofpoint (curve t)))) (makepoint ( (* cth x) (* sth y)) (+ (* sth x) (* cth y))))))) (scalexy unitline (/ (sqrt 2) 2) (/ (sqrt 2) 2)) 45) 0.0)Using Application Rule 2:(let ((cth (cos (degreestoradians 45))) (sth (sin (degreestoradians 45)))) (lambda (t) (let ((x (xofpoint ((scalexy unitline (/ (sqrt 2) 2) (/ (sqrt 2) 2)) t))) (y (yofpoint ((scalexy unitline (/ (sqrt 2) 2) (/ (sqrt 2) 2)) t)))) (makepoint ( (* cth x) (* sth y)) (+ (* sth x) (* cth y)))))))We only care how many times unitline is evaluated. Here, we see it is used twice the same way: ((scalexy unitline (/ (sqrt 2) 2) (/ (sqrt 2) 2)) t) to get x and y. So, the total number of times unitline is evaluated will be twice the number of times it is evaluated to evaluate ((scalexy unitline (/ (sqrt 2) 2) (/ (sqrt 2) 2)) t). By Evaluation Rule 2 we replace scalexy:(((lambda (curve xscale yscale) (lambda (t) (makepoint (* xscale (xofpoint (curve t))) (* yscale (yofpoint (curve t)))))) unitline (/ (sqrt 2) 2) (/ (sqrt 2) 2)) 0.0)Application Rule 2:((lambda (t) (makepoint (* (/ (sqrt 2) 2) (xofpoint (unitline t))) (* (/ (sqrt 2) 2) (yofpoint (unitline t))))) 0.0)We use Evaluation Rule 3 and Application Rule 2 again:(makepoint (* (/ (sqrt 2) 2) (xofpoint (unitline 0.0))) (* (/ (sqrt 2) 2) (yofpoint (unitline 0.0))))So, unitline is evaluated twice to evaluate ((scalexy unitline (/ (sqrt 2) 2) (/ (sqrt 2) 2)) t), which is evaluates twice to evaluate (rotatearoundorigin (scalexy unitline (/ (sqrt 2) 2) (/ (sqrt 2) 2)) 45). This is consistent with our trace result that unitline is evaluated 4 times when we apply ((gospercurve 1) 0.0).When we apply ((gospercurve 1) 0.6) we saw unitline was evaluates 8 times. The reason is because the second branch of the if it taken in connectrigidly: Using application rule 2:
(lambda (t) ; body of connectrigidly (Evaluation Rule 2) (if (< t (/ 1 2)) ((rotatearoundorigin (scalexy unitline (/ (sqrt 2) 2) (/ (sqrt 2) 2)) 45) (* 2 t)) ((translate (rotatearoundorigin (scalexy unitline (/ (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) (makepoint (+ x (xofpoint (curve t))) (+ y (yofpoint (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 unitline. So, applying it twice will require 8 evaluations.We'd be here an awfully long time if we tried to analyze (gospercurve 5) the same way! Using trace, we can see that ((gospercurve 5) 0.0) evaluates unitline 1024 times (= 2^{10}). This make sense  each time we apply gosperize, it quadruples the number of applications of unitline to evaluate the resulting curve at t value 0.0 since it applies rotatearoundorigin and scalexy to the curve, each of which evaluate the curve twice. Quadrupling five times is (* 4 4 4 4 4) = 1024.
But what about ((gospercurve 5) 1.0)? For the second branch of the if, each gosperize level octuples the number of applications of unitline! So, we will have (* 8 8 8 8 8) = 32768 = 2^{15} evaluations of unitline. Applying ((gospercurve 5) t) to tvalues betwen 0.0 and 1.0 will evaluate unitline some number between 1024 and 32768 times.
The total number of evaluations ot unitline to draw (showconnectedgosper 5) is 62058496 (I found this by using state to add a counter to unitline, 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 gospercurve 5 we worked out above! I'll leave it to you to figure out why. (Hint: we aren't just evaluating (gospercurve 5) in showconnectedgosper, what does squeezerectangularportion do?)
> (time (showconnectedgosper 5)) cpu time: 362081 real time: 362081 gc time: 0After using let to eliminate all the duplicate evaluations, we should be able to evaluate (gospercurve 5) with only one evaluation of unitline. If we redefine translate, scalexy and rotatearoundorigin to use let we eliminate the duplicate evaluations:(define (translate curve x y) (lambda (t) (let ((ct (curve t))) (makepoint (+ x (xofpoint ct)) (+ y (yofpoint ct)))))) (define (scalexy curve xscale yscale) (lambda (t) (let ((ct (curve t))) (makepoint (* xscale (xofpoint ct)) (* yscale (yofpoint ct)))))) (define (rotatearoundorigin curve theta) (let ((cth (cos (degreestoradians theta))) (sth (sin (degreestoradians theta)))) (lambda (t) (let ((ct (curve t))) (let ((x (xofpoint ct)) (y (yofpoint ct))) (makepoint ( (* cth x) (* sth y)) (+ (* sth x) (* cth y))))))))Evaluating showconnectgosper evaluates unitline 2000 times:> (time (showconnectedgosper 5)) cpu time: 341 real time: 340 gc time: 0 2000We're really only drawing 1000 points, though, so it is still doing twice as much work as necessary. The problem is drawcurveconnected used to show the Gosper curve. Figuring out how to make this only evaluate unitline 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 unitline:
> (exact>inexact (/ 62058496 2000)) 31029.248The reason is that for the 2000point curve, we are spending a lot of time on things other than just evaluating unitline (such as actually drawing the points on the window).
University of Virginia Department of Computer Science CS 200: Computer Science 
David Evans evans@cs.virginia.edu Using these Materials 