Lecture 26: Invariants, Iteration, and Choices



  1. Invariants and the design of accumulators

    Why accumulators:

    1. follow a structural or generative design recipe
      discover need for "accumulator"
      add accumulator
    2. follow a structural or a generative design recipe
      understand that several results are needed at once
      translate design into accumulator-style and accumulate several different results at once.

    What is accumulated:

    (define (f x so-far)
      (cond
        ((empty? x) ... so-far ...)
        (else (f (rest x) (restore-relation (first x) so-far)))))
    

    Consider how a program processes a list of items:
    Step i:
    
      ---*---*---*---*---*---*---*---*---*---*---*---*---*---*---*---*---*---
    			 ^
    	  seen           |       to be processed 
    
    The goal of an accumulator is to maintain a fixed relationship between "seen" and "so-far". Say f multiplies all the numbers in the list. Then the relationship is (PI seen) = so-far. Now, if we move the pointer over by one, we have changed what we have "seen":
    Step i+1: 
    
                             X
      ---*---*---*---*---*---*---*---*---*---*---*---*---*---*---*---*---*---
    			     ^
    	  seen               |   to be processed 
    
    Moving the pointer over means "calling a(nother) function" [same or other]. But now we have seen more and a new value for "so-far" should reflect this. We need a function that can use "X" and "so-far" to restore the relationship. In our running example, the function is *.

    Watch:

      (PI-A (list 1 2 3) 1)
    = (PI-A (list 2 3)   (* 1 1))
    = (PI-A (list 2 3)   1)
    = (PI-A (list 3)     (* 2 1))
    = (PI-A (list 3)     2)
    = (PI-A (list)       (* 3 2))
    = (PI-A (list)       6)
    = 6
    

    The relationship is often called an invariant because it does not change over the course of the computation.

  2. Iterative behavior:

    Compare the following two factorial functions and how they compute their results:
    (define (! n)
      (cond
        ((zero? n) 1)
        (else (* n (! (sub1 n))))))
    
    (define (!-A n so-far)
      (cond
        ((zero? n) so-far)
        (else (!-A (sub1 n) (* n so-far)))))
    
      (! 3)
    = (* 3 (! 2))
    = (* 3 (* 2 (! 1)))
    = (* 3 (* 2 (* 1 (! 0))))
    = (* 3 (* 2 (* 1 1)))
    = 6
    
      (!-A 3 1)
    = (!-A 2 3)
    = (!-A 1 6)
    = (!-A 0 6)
    = 6
    

    The left one builds up a context around the recursive calls (see red applications). It is "properly recursive." The right one does not build up context. It is called "iterative".

    Transforming a properly recursive function into an accumulator version typically yields an iterative function. Unfortunately, it is not always "equivalent". To achieve that: take 311!

    Not all accumulator-style functions are "iterative".

    Not all "iterative" functions are accumulator-style functions.

    The terms are historical but unfortunately important. Many programming languages still provide constructs for "iteration" because their designers did not understand that functions can be perfectly iterative. These constructs are called "looping constructs" or "loops". Indeed, many programming courses focus on squeezing computation into one or two select looping constructs, instead of matching the shape of "procedures" and "data definitions."

    Try it with:
    (define (f alon)
      (cond
        ((empty? alon) 1)
        (else (* (first alon) (f (rest alon))))))
    
    (define (g alon a)
      (cond
        ((empty? alon) a)
        (else (g (rest alon) (* (first alon))))))
    
    Which one is iterative? Can you tell without hand-evaluation?

  3. The Landscape of Choices
              domain knowledge 
    
              structural processing with parallel "complex" arguments 
    	  -- choose one as primary or process both in parallel
    
              structural       generative 
    w/o  accu 
    
    with accu 
    
              [mutually recursive data defs]
    

    Here is a choice:

       zipper : (listof number) (listof number) -> (listof number)
       (define (zipper l1 l2) ...)
    
       Assumption: both l1 and l2 are sorted in ascending order
       and are free of duplicates; however, the same number may 
       occur on l1 and l2
    
       Purpose: produce a single list of all numbers in l1 and l2
       in ascending order; if l1 and l2 each contain the same number
       only include one in the result
    
    We can either think of zipper as

    If the input list contains N items, the first one will need around N * N/2 recursive calls to insert (N to zipper and for each of those N/2 to insert). For the parallel argument version, however, it is around 2 * N. No play this for N in 10, 100, 1000 ... It is obvious that we want the parallel version. (212 teaches more concerning such comparisons.)





Matthias Felleisen This page was generated on Fri Apr 9 09:17:38 CDT 1999.