Why accumulators:
What is accumulated:
(define (f x so-far) (cond ((empty? x) ... so-far ...) (else (f (rest x) (restore-relation (first x) so-far)))))
Step i: ---*---*---*---*---*---*---*---*---*---*---*---*---*---*---*---*---*--- ^ seen | to be processedThe 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 processedMoving 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.
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)))))) |
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 resultWe 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. |