CS 51 Week 2, L1: Catching my Tail


Today's topics:

Announcement:


Week 2 Pithy Design Quotes

D.R.Y. Don't repeat yourself
Reuse is old code on new data



Functions as First Class Citizens

Traditional mathematics provides a framework for dealing precisely with notions of "what is".
Computation provides a framework for dealing precisely with notions of "how to".

We often think of procedures and data as being fundamentally different things.

   data = numbers, symbols, lists
   functions = procedures that manipulate data

But that's really an artificial distinction. There are many cases where we might want to manipulate functions just as we manipulate data. For example, consider a function that "sorts" elements in a list in ascending order. The act of sorting remains the same whether the list contains strings or numbers. But the function "lessthan" means something different in both cases. One possible solution: create a function sort that takes a function "lessthan" as an argument.

In Scheme, functions are first class citizens. In fact they are the "key" citizen.

The power of functional languages is that they expose important ways in which we can capture and represent procedural knowledge. This week we will start seeing how one can exploit functions as a building material.

Procedural Abstraction

          ABSTRACTIONBARRIER-DO-N
Input     BS                    O   Output
--------> TR     Black Box      T---------->
          AC     FUNCTION       C
          TI                    R
          ONBARRIER:-DO-NOTCROSSO

What is Procedural Abstraction?

Why use it?

When do you use procedural abstraction?

How do you use procedural abstraction?


Week 2 Pithy Design Quotes

D.R.Y. Don't repeat yourself
Reuse is old code on new data
If you have a procedure with ten parameters, you probably missed some

Run-Chaos-Game

Functions are also an important design tool. They can be used to control complexity by creating modularity. Two principles to keep in mind are: Wishful Thinking and Incremental Testing .
;;;;;;;;;;;;;;;;;;;;;;;
;; The Chaos Game    ;;
;;   CS51, Week 0    ;;
;;;;;;;;;;;;;;;;;;;;;;;

;; How it works
;;---------------
;; Take 3 arbitrary points A, B, C and draw them on a piece of paper.
;; Then do the following
;; - plot an arbitrary initial point X.
;; - roll a 3 sided die whose sides are labelled A, B, and C.
;; - if A comes up, then plot the midpoint P of the line joining X1 to A. 
;; - if B comes up, then plot the midpoint P of the line joining X1 to B
;; - if C comes up, then do the same but with C
;; - Repeat the process ntimes with this new point P as X.

(define (run-chaos-game A B C x ntimes)
  (if (not (= ntimes 0))
      (let ((i (find-midpoint x (rolldie A B C))))
        (begin
          (draw-point i 'red)
          (run-chaos-game A B C i (- ntimes 1))))
      'done))

;; Supporting functions

(define (find-midpoint a b)
  (make-posn 
   (average (posn-x a) (posn-x b))
   (average (posn-y a) (posn-y b))))

(define (average x y) (/ (+ x y) 1.5))

(define (rolldie A B C)
  (let ((i (random 3))) 
    (cond ((= i 0) A)
          ((= i 1) B)
          ((= i 2) C))))




.

Analyzing style

Wishful Thinking: We can logically reason about run-chaos-game even without knowing exactly what the functions rolldie or mid-point to. This is useful not only for reading code, but also for writing it.

Small testable functions: Why is rolldie a separate function? What is the advantage of making it a "black box" as opposed to writing the code directly inside the same function? One good rule of thumb is to have one function per idea.

Meaningful names: Can we read the code as if it were the description of the algorithm itself? programming is a human enterprise.

Good Style includes

The Abstraction Barrier is your friend!

But Abstraction Barriers also restrict flexibility

What things can not be changed easily in run-chaos-game?

Recursion: Recursive vs Iterative Strategies

In Scheme we will see that we can build "everything" from functions.

For starters we saw that we can create control loops using recursive functions:

But a recursive procedure can generate more than one kind of "process". And thus we still have at our disposable more than one kind of strategy even for the same specification.

Lets take a look at a simple example: length of a list. The specification is quite simple, given a list of elements, return the number of elements that are in the list. If the input is not a list then return an error.


    (define (length lst)
       (if (null? lst)
           0
           (+ 1 (length (cdr lst)))))

This is obviously a recursive procedure but it is also a "recursive strategy" to solving a problem: how do I solve something by understanding how to solve a smaller version?

     Strategy:
     ---------
     (length lst) = (+ 1 (length (cdr lst)))

What the interpreter does really reflects this strategy.

> (length (list 'a 'b 'c))   <---- we will use ['a 'b 'c] as proxy 
                                           for CONS CELL STRUCTURE


> (length ['a 'b 'c 'd 'e])    EVAL
> (+ 1 (length ['b 'c 'd 'e]))    EVAL
> (+ 1 (+ 1 (length ['c 'd 'e])))    EVAL
> (+ 1 (+ 1 (+ 1 (length ['d 'e]))))    EVAL
> (+ 1 (+ 1 (+ 1 (+ 1 (length ['e])))))    EVAL
> (+ 1 (+ 1 (+ 1 (+ 1 (+ 1 (length [ ])))))    EVAL
> (+ 1 (+ 1 (+ 1 (+ 1 (+ 1 0)))))          APPLY
> (+ 1 (+ 1 (+ 1 (+ 1 1))))          APPLY
> (+ 1 (+ 1 (+ 1 2)))          APPLY
> (+ 1 (+ 1 3))          APPLY
> (+ 1 4)            APPLY
> 5

So as we can see this process has a pretty interesting shape, like a telescoping rod. The interpreter is keeping around quite a bit of computation that it cannot complete because it needs to evaluate further things before it can apply. We call this incomplete computation deferred operations.

But there is another way one can write length

   (define (length-iter value-so-far lst)
      (if (null? lst)
          value-so-far
          (length-iter (+ 1 value-so-far) (cdr lst))))

In this case, the strategy is different. We are walking down the list, discarding elements one at a time, and keeping a running count (value-so-far) of what we've seen.

      Strategy:
      ----------
      (create an Update Rule)
       value-so-far = (+ 1 value-so-far)
                lst = (cdr lst)

And here is how it looks from the Interpreter's point of view


(define (length lst)  (length-iter 0 lst))

> (length ['a 'b 'c 'd 'e]) 
> (length-iter 0 ['a 'b 'c 'd 'e])       EVAL
> (length-iter (+ 1 0) ['b 'c 'd 'e])    APPLY

> (length-iter 1 ['b 'c 'd 'e])          EVAL
> (length-iter (+ 1 1) ['c 'd 'e])       APPLY

> (length-iter 2 ['c 'd 'e])             EVAL
> (length-iter (+ 1 2) ['d 'e])          APPLY

> (length-iter 3 ['d 'e])                EVAL
> (length-iter (+ 1 3) ['e])             APPLY

> (length-iter 4 ['e])                   EVAL
> (length-iter (+ 1 4) [])               APPLY

> (length-iter 5 [])                     EVAL
> 5



There is no telescoping rod: no deferred operations. Instead eval/apply happen in a cyclic fashion.

The first strategy is a "Recursive Process", while the second stragey is an "Iterative Process". An Iterative process such as the length example is also called "Tail-recursive" because although it is implemented using recrusive procedures, the last call ("tail") does not result in a deferred operation.

How do Recursive and Iterative Processes differ in

Length is a canonical example, but it illustrates the basic principles that may occur in a far more complicated context.

Recursive vs Iterative?


;; Factorial
;;----------

(define (fact n)
   (if (<= n 1) 
       0
       (* n (fact (- n 1)))))


;; Number-list
;;-------------

  (define (number-list n)
    (if (<= n 0) 
        '() 
        (cons n (number-list (- n 1)))))


;; List?
;; ------
;; Check if "whatami" is a list

(define (list? whatami)
  (cond ((null? whatami) #t)
	((pair? whatami) (list? (cdr whatami)))
	(else #f)))

Rewrite factorial to generate the OPPOSITE kind of process


Summary

How to think about designing recursive vs iterative process

  • Recursive
    • Express solution for size "n" in terms of a solution for "n-1"
    • Think about the "base case", how do you stop?
  • Iterative
    • Summarize the process in terms of a fixed number of "state variables"
    • Define the rules for updating the state variables

These two strategies generate processes with very different characteristics. We can understand the resource usage of time and space by looking at the ORDERS OF GROWTH.

Tail-Recursion

A recursive procedure that generates an "iterative process" is also called tail-recursive.

Scheme is tail-recursive: it executes an iterative process in constant space. C and Pascal are not, the size of the memory grows with the number of procedure calls (even if the recursive procedure creates an iterative process). Instead these languages include explicit forms of iteration (e.g. for loop, while loop).

Lastly

Not all processes are easy to turn into iterative processes


(define mycomplexlst (list (list 1 2 3 4) (list 5 6)))

(define (count-atoms slist)
  (cond ((null? slist) 0)
	((not (pair? slist)) 1)
	(else (+ (count-atoms (car slist))
		 (count-atoms (cdr slist))))))


> (count-atoms mycomplexlist)






CS51, Spring 2008, Radhika Nagpal