COMP203: Lecture 14


Syllabus | Homework and Assignments | Grading Rubric | Midterm Exam | Final Project

When circumstances allow, I'll be typing up bits of my lecture notes and posting them online. These may or may not bear any resemblance to the actual lectures.


Recursion, continued

Last class we saw a few examples of recursive programs -- programs that use themselves as helper procedures and so have a loop in their flowcharts. We learned that all recursive programs must have a "stop condition" to keep them from going into an infinite loop. Another key feature of recursive programming is what the author calls the "leap of faith". When you're writing a procedure that will be recursive, you have to assume that the procedure will work when used as a helper procedure.

Here's the example we saw last class:

to initials.recursive :name
 ifelse (emptyp :name)~
    [output []]~
    [output sentence (first (first :name)) (initials.recursive butfirst :name)]
end
The procedure stops when variable name contains the empty list. The leap of faith is the assumption that initials.recursive can successfully output a list of initials from every item except the first in the list contained in name. Also notice that initials.recursive uses itself as a helper procedure unless the stop condition is met, and that it checks the stop condition before using itself as a helper.

Induction and Recursion

As mathematicians, you may have seen this sort of leap of faith in the process of proof by induction. The principle of proof by induction is that if you've proven something true for the base case (analogous to the stop condition) and if, by assuming it's true for some integer k, you can prove it's true for k+1, then that statement must be true for all positive integers. This inductive step is analogous to the leap of faith in recursion.

If you're familiar with proof by induction, you may wish to view this proof to see how the various steps align with the features in a recursive procedure.

If you haven't seen proof by induction yet, consider the following "recursive definition":

(n+1)! = (n+1) * n! (leap of faith)
0! = 1 (stop condition).

Let's use this definition to calculate the value of 3!. We don't know it directly, but we do know that 3! = 3 * 2!. Again, we don't know the value of 2!, but we can use the definition to learn that 2! = 2 * 1!. The definition says that 1! = 1 * 0!, and we've now reached our "stop condition".

We know that 0! = 1, so 1! must be 1 * 1 = 1. So 2! = 2 * 1! = 2 * 1 = 2, and 3! = 3 * 2! = 3 * 2 = 6. This is exactly how a recursive procedure would compute 3!.

Try writing a recursive procedure named factorial that accepts a positive number n as input and outputs the value n!. You may wish to use procedure initials.recursive, above, as an example.

Important note: in order to use 2! to calculate the value of 3!, we had to know the value of 2!. Your procedure will have to output its result (not print it) in order to use that result in future calculations.

Recursion and Graphics

As we saw with the example of the tree on page 193 of Chapter 10, recursive procedures can be used to draw very pretty pictures. The extra credit problem of assignment 5 shows one such picture made of triangles. Here we'll talk about how to write a recursive procedure to draw a similar design with squares.

The hardest part of the problem is the leap of faith. Suppose we already had a procedure that could draw a small collection of nested squares. How would we write a procedure to add one square to that collection?

What information do we need? We need the size of the new square that we're drawing, the size of the collection of squares to be drawn inside that square, and information on where to position the smaller collection of squares inside the large outer square.

We can make the size of the square we're drawing an input to our recursive procedure:

to squareview :size
;
;
;
;
end
The size of the collection of squares inside the starter square will depend on the size of the starting square. After some thought, you may notice a 45-45-90 triangle at the corner of each square. If the two legs of that triangle have length :size/2, the Pythagorean theorem says that the hypotenuse must have length :size/sqrt(2).

to square :size
 repeat 4 [forward :size right 90]
end

to squareview :size
 square :size
 ;
 ;
 squareview :size / (sqrt 2)
end
Our procedure isn't done yet. What are we missing?

  1. We need a stop condition. There is more than one right stop condition, but we must be sure that that stop condition is eventually reached. Our two chief options are: stop after we've drawn a certain number of squares, or stop when the squares get small enough. The second option seems simpler and more aesthetically pleasing to me, so I'll choose that one.

    The size of the squares is dictated by the variable size, so we just stop when the squares get small enough.

    to squareview :size
     square :size
     if lessp :size 2 [stop]
     ;
     squareview :size / (sqrt 2)
    end
    
    (What happens if we mistakenly say if lessp 2 :size? Why don't we need an output command as we did with the factorial program?)

  2. We need to move the turtle into position to draw the smaller collection of squares. (If we do this step second, we can even run the procedure to see why we need this!)

    First, figure out where your turtle will be when it finishes drawing the outside square. Then figure out where it needs to move to to draw the next square.

    In this example, the turtle needs to move forward :size/2 distance, then turn to the right by 45° before starting the inside square.

    to squareview :size
     square :size
     if lessp :size 2 [stop]
     forward :size/2 right 45
     squareview :size / (sqrt 2)
    end
    
Remember that the command ht hides the turtle.