Announcement:
|
Week 2 Pithy Design Quotes D.R.Y. Don't repeat yourself Reuse is old code on new data |
Traditional mathematics provides a framework for dealing precisely with notions of "what is". |
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.
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 |
;;;;;;;;;;;;;;;;;;;;;;;
;; 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?
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
How to think about designing recursive vs iterative process
|
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)