Last class, we saw our first examples of generative recursion.  We are
going to start this class by discussing some aspects of the
relationship between structural and generative recursion.

First, the design recipe.  How does our design recipe for structural
recursion carry over to generative recursion?  The structural design
recipe had the following steps:

1. Data analysis and design, including examples of data
2. Contract, purpose, header
3. Examples/Test cases
4. Template
5. Body
6. Test

Which of these still make sense in generative recursion?  Actually,
everything but the template does.  We still need to understand our
data, even if we plan to process it generatively.  Similarly,
contracts, purposes, and headers apply to all programs.  Therefore, we
can reuse most of the design recipe, with a few small modifications.

First, when we write examples, we should write two kinds of examples:
our usual test cases are one kind.  The other kind are examples of how
the program operates (as in the diagrams that we drew in class to
demonstrate qsort). 

Second, the template changes a bit, as we discussed in the last
class.  The new template takes the following form (from page 357 of
text): 

(define (generative-recursive-fun problem-data)
  (cond 
   [(trivially-solvable? problem-data) 
    (determine-solution problem-data)] 
   [else 
    (combine-solutions 
     problem-data 
     (generative-recursive-fun (generate-problem1 problem-data)) 
     ...
     (generative-recursive-fun (generate-problemN problem-data)))])) 

This is a slightly different style of template.  In the old template,
we just plugged in holes.  In the new template, we replace the parts
of the template with code that does what that part of the template
requires.  What sorts of questions are useful to ask?

- What is a trivially solvable problem?
- What is the corresponding solution?
- How do we generate new problems that are easier to solve than the
  original problem?
- Do we generate one new problem or several new problems?
- Is the solution to a new problem the solution of the original
  problem, or do we need to combine results from new problems?

Answering these questions tells us how to fill in the template for
generative recursive programs.

This gives us a revised design recipe for programs that use generative
recursion: 

1. Data analysis and design, including examples of data
2. Contract, purpose, header
3. Test cases and examples of the program
4. The Generative Template
5. Body
6. Test

With this in hand, let's write another program.  We'll write a program
to implement a simple number guessing game.  The program consumes two
numbers (a low value and a high value) and produces a number.  The
program runs until it guesses a "hidden" number; when it guesses
correctly, it returns the "hidden" number.  Most of us have played
this game before: we guess the number in the middle to get to the
hidden number as quickly as possible.  This approach is known as
binary search; it arises frequently in programming problems.

Assume we have a helper function guess that takes a number, compares
it to the hidden number, and returns one of 'higher, 'lower, or 'equal,
depending on the relative value between the input number and the
hidden number.

;; guess: num -> 'higher or 'lower or 'equal
;; Given a number, tells if the hidden number is higher, lower, or equal.

;; hi-lo: int int -> int
;; Given a low & high bound, return the hidden number (in [lo,hi]).
(define (hi-lo lo hi)
  (local [(define mid (/ (+ lo hi) 2))
	  (define answer (guess mid))]
    (cond [(symbol=? answer 'equal) mid]
	  [(symbol=? answer 'higher) (hi-lo (truncate mid) hi)]
	  [(symbol=? answer 'lower)  (hi-lo lo (truncate mid))])))
				   
Let's try this program, with a hidden number of 3
  (hi-lo 0 15) mid=7.5
= (hi-lo 0 7)  mid=3.5
= (hi-lo 0 3)  mid=1.5
= (hi-lo 1 3)  mid=2
= (hi-lo 2 3)  mid=2.5
= (hi-lo 2 3)  now we're in an infinite loop

What went wrong here?  Our method for generating new problems failed
to generate a new problem -- it generated an old problem again, so the
program fails to terminate.  Why was this never a problem in
structural recursion?  Because recursive calls always operated on a
smaller piece of data (such as the rest of the list) than the original
call.  This example demonstrates that, when using generative
recursion, we can more easily write programs that fail to terminate.
This is bad, and requires an extra piece of documentation in the
design recipe: a termination argument.  

The termination argument should explain why your divide-and-conquer
approach will eventually yield a trivially-solvable problem.  You
should write it as a comment just after the description of the program
examples.  What could we use as a termination argument on sierpinski
from last class?  Something like the following would do: 

  "at each step, sierpinski partitions the input triangle into three
   triangles whose sides are strictly smaller than those of the
   original.  The trivial problem test checks whether the sides are
   smaller than a certain threshold.  Since the sides grow smaller
   with each recursive call, the trivial problem test must eventually
   succeed, so the algorithm must terminate."

How would we fix hi-lo such that we could write a reasonable
termination condition?  One idea is to check whether the number we are
trying to guess is one of the endpoints:

(define (hi-lo lo hi)
  (cond [(symbol=? (guess lo) 'equal) lo]
        [(symbol=? (guess hi) 'equal) hi]
        [else (local [(define mid (/ (+ lo hi) 2))
		      (define answer (guess mid))]
		(cond [(symbol=? answer 'equal) mid]
		      [(symbol=? answer 'higher) (hi-lo (truncate mid) hi)]
		      [(symbol=? answer 'lower)  (hi-lo lo (truncate mid))]))]))

What would the termination argument look like?  Something like:

  "at each step, hi-lo checks whether the number to guess is one of
   the endpoints of the range.  If not, then the number must be
   strictly between the endpoints.  If there is a number strictly
   between the endpoints, then the recursive call to hi-lo must be
   over a strictly smaller range.  If the range gets strictly smaller
   on each call, it must eventually find the number, or reach a case
   where the endpoints are one number apart.  At that point, the
   number to guess must be one of lo or hi and the algorithm will
   reach the trivially solvable case and terminate."

Here's another way that we could have fixed hi-lo.  Notice in the
trace we did earlier that 3 showed up as the boundary point early in
the computation.  Why didn't our check of mid catch this?  Because mid
had value 3.5 instead of 3.  If we let mid take non-integer values,
we're guaranteeing that guess can't produce 'equal.  What if we
instead wrote hi-lo to check the truncated mid, instead of mid?

(define (hi-lo lo hi)
  (local [(define mid (truncate (/ (+ lo hi) 2)))
	  (define answer (guess mid))]
    (cond [(symbol=? answer 'equal) mid]
	  [(symbol=? answer 'higher) (hi-lo mid hi)]
	  [(symbol=? answer 'lower)  (hi-lo lo mid)])))
				 
  (hi-lo 0 15) mid=7
= (hi-lo 0 7)  mid=3
= 3 (because guess returned 'equal)

What's the termination argument for this version?  It should be
somewhat similar.  Notice that after each call, the new values of lo
and hi are either the original endpoints or values of mid.  Since we
check our guess against each value of mid, we should always terminate.
Thus, this version should give us the desired behavior, without our
having to check the values of lo and hi on each pass.

This argument has a flaw though.  If an endpoint came from a
computation of mid, we do indeed check it.  What if our guess is one
of the endpoints though?  Will the program still terminate?  Let's try 
it with hidden number 0 in the range 0..2

  (hi-lo 0 2) mid=1
= (hi-lo 0 1) mid=0
= 0

This seems fine.  What's happening here?  When the range gets to two
consecutive numbers, mid becomes the lower number.  Therefore, the
original lo endpoint is not a problem.  What about the original hi
endpoint?  Let's try again with the hidden number being 2.

  (hi-lo 0 2) mid=1
= (hi-lo 1 2) mid=1
= (hi-lo 1 2) ... oops

So, this tells us that we need a special case for the original hi
endpoint, but the current algorithm will catch all other guesses.
Here's the corrected program with a termination statement.

;; hi-lo: int int -> int
;; Given a low & high bound, return the hidden number (in [lo,hi]).
;;
;; Termination: On each recursive call to hi-lo-help, lo and hi are
;; either one of the original endpoints or a value computed as mid.
;; The program explicitly checks all values of mid and the original hi
;; endpoint against the guess.  Thus, the program will terminate if
;; the hidden number must become one of these values.  On each
;; recursive call, the range between lo and hi gets smaller and always
;; includes the hidden number.  Thus, the smallest case we can reach
;; has lo and hi being consecutive numbers.  If the hidden number is
;; lo, the algorithm will terminate when it calculates mid because the
;; mid of lo and lo+1 is lo.  If the hidden number is hi, the
;; algorithm will terminate because the hi endpoint is either the
;; original hi endpoint (which we checked explicitly) or some value of
;; mid, which we also checked explicitly.

(define (hi-lo lo hi)
  (local [(define (hi-lo-help lo hi)
	    (local [(define mid (truncate (/ (+ lo hi) 2)))
		    (define answer (guess mid))]
	      (cond [(symbol=? answer 'equal) mid]
		    [(symbol=? answer 'higher) (hi-lo-help mid hi)]
		    [(symbol=? answer 'lower)  (hi-lo-help lo mid)])))]
    (cond [(symbol=? (guess hi) 'equal) hi]
	  [else (hi-lo-help lo hi)])))

Yet another common way to fix hi-low would be to have the recursive
calls exclude mid from the new range, as follows:

(define (hi-lo lo hi)
  (local [(define mid (truncate (/ (+ lo hi) 2)))
	  (define answer (guess mid))]
    (cond [(symbol=? answer 'equal) mid]
	  [(symbol=? answer 'higher) (hi-lo (add1 mid) hi)]
	  [(symbol=? answer 'lower)  (hi-lo lo (sub1 mid))])))

This version has the advantage of an easier termination condition.  

;; Termination: The range between the numbers gets strictly smaller on
;; every iteration.  In the worst case, the hi and lo values
;; eventually become the same (in which case the hidden number is
;; hi=lo=mid), in which case the algorithm terminates.

This exercise demonstrates the benefit in thinking about termination
justifications. In this case, thinking about termination located an
error in our program.  The extent to which you think about termination 
conditions and other aspects of whether your programs are correct
determine whether you are a recreational or professional programmer.

Many students are uneasy with the creative aspect of generative
recursion: if you don't see how to divide the problem, you can't write 
a generative recursive solution.  Generative recursive programs span a 
whole spectrum of problems, some for which the divide-and-conquer step 
is easy (as in hi-lo), and others for which it takes real genius and
insight.  This makes generative recursive solutions both fun and
sometimes difficult.

How often will you need to write generative recursive programs?  Most
problems have structural recursive solutions.  Consider sort.  In lab, 
you wrote a sort using structural recursion.  The generative recursion 
sorting algorithms are faster, but that might not matter to your
context.  As a rule of thumb, look for structural recursive solutions
first.  If they prove to be too slow for your needs, try to think of a 
generative solution.  Otherwise, use the structural solution and be
content that it took you less time to develop.

We spent the rest of class starting on find-root, which is described in
detail starting on page 376 of the text.