CS 1321X - Lecture 22 - November 4, 2003

CS 1321X - Lecture 22

The Road to Perdition


I. Iteration

So far, we've seen one way of getting repetitive behavior: recursion.
The idea behind recursion was to get a set of instructions executed
repeatedly by having a function call itself. For example if function 
foo says "do this" and "do that" and then calls itself, you see "do 
this" and "do that" done over and over again:

do this
do that
call foo
      do this
      do that
      call foo
           do this
           do that
           call foo
                do this
                ...and so on...

Now that we have some non-functional knowledge of variables and
assignment, however, we can introduce another means of getting
repetitive behavior. Almost all programming languages (except those
which are purely functional) give us this alternative, which is called
iteration. The control structure that gives rise to iteration is
called a "loop". Loops let us execute a set of instructions over and
over again without recursive function calls. Instead, a loop says
"here's a set of instructions to execute...do them in order, and when
you get to the end, go back to the first instruction and start again...keep
doing this until somebody tells you to stop." So that loop behavior, in 
"do this...do that" terms looks like this:

do this
do that
go back to the beginning
do this
do that
go back to the beginning
do this
do that
go back to the beginning
do this
...and so on...

We didn't talk about terminating conditions in either of the crude
examples above, but of course they're absolutely necessary. Recall
that if we didn't encounter a termination condition with recursion, 
our computer would grind to an abrupt halt with a "stack overflow" 
when the activation stack ran out of room to push new activation 
frames on the stack. When you fail to encounter a termination 
condition in a loop, you run into a phenomenon called the "infinite 
loop". As the name implies, this is a loop that executes forever; 
since there are no recursive function calls, there's no growth of the 
activation stack, so the computer won't halt because of a memory 
shortage. So the infinite loop just runs and runs and runs until 
somebody or something intervenes.

So just like recursion, iterative forms (i.e., loops) requre a
termination condition. Similarly, both recursion and iteration
require that something happens within the instructions being executed 
repeatedly that gets the problem closer to that termination 
condition. And while a recursive procedure requires the direct (or 
indirect) call to itself, an iterative form requires an explicit or 
implicit command to go back to the beginning of the given set of 
instructions and start again.


II. How Scheme does iteration

Scheme provides you with one slightly complicated iterative form.
It's just called "do", and it looks like this:


 (do (([var-1] [init-expr-1] [update-expr-1])
      ([var-2] [init-expr-2] [update-expr-2])
                   :
                   :
      ([var-n] [init-expr-n] [update-expr-n]))
     ([test-expr] [action-expr-1] ... [action-expr-m])
                   :
         [zero or more expressions]
                   :
                                )

Here's how "do" works:

1.   each [var-i] is bound to its corresponding [init-expr-i]
     (in "parallel", just like with "let"---that is, there's
     no guarantee of the order the bindings are completed,
     so don't encode dependencies of any kind in this part
     of the "do" form).

2.   [test-expr] is evaluated. If the result is non-#f, then
     the [action-expr-1] through [action-expr-m] are evaluated
     in left-to-right fashion. The "do" form returns the value
     of [action-expr-m] and the iteration is terminated. If
     there are no action expressions, what the "do" form
     returns is unspecified (!).

3.   if evaluating [test-expr] returns #f, then execute the
     body of code.

4.   when the body of code has been executed, update each
     [var-i] by binding it to the value obtained by evaluating
     the corresponding [update-expr-i]. The [update-expr-i]
     are optional, so if it's not there, then the various
     [var-i] are never changed. Also, these bindings are done
     automatically by "do", so there's no need to include your
     own "set!" in the update expressions. Finally, these
     updates, as well as the initializations, are done in
     an unspecified order, so you can't count on any
     dependencies (i.e., this happens before that). You
     can however count on some protection in that Scheme binds
     the variables to fresh locations, does the updates to
     those fresh locations, and then binds the updated values
     to the variable.

5.   go to 2


III. Your first iterative example, revisited

Hey boys and girls! I've got a great idea...let's compute factorials
again!! Well, factorial was the first repetitive execution example we
did, so it might be worthwhile to explore how we'd compute factorials 
without recursion.

 (define (factorial n)
     (do ((result 1)   ;; this is where we'll compute the
                       ;; product
          (counter n)) ;; this is where we'll count how
                       ;; many times we've done this loop
       ((= counter 0) result)           ;; if we've done it n
                                        ;; times, then quit
       (set! result (* result counter)) ;; accumulate product
       (set! counter (- counter 1))))   ;; move problem closer
                                        ;; to termination



Now as we noted above in the description of the "do" form, we can do 
lots of the "updating" work in the "preamble" of the do. In the
case of factorial, we can do it all:

 (define (factorial n)
     (do ((result 1 (* result counter))
          (counter n (- counter 1)))
       ((= counter 0) result)
       ;; no expressions here
       ))


Some programmers think that this is elegant programming style.
Frankly, it often confuses me; I try to be real obvious with what I'm
doing in a program. But you can write your iterative code either way if
you want (just don't ask me to debug it if you put all that stuff in 
the preamble).


IV.  Another example: iterating down a list

Let's say that we want to find the average of a list of numbers...maybe 
they're somebody's quiz scores. For every element of the list (which 
we'll assume is all numbers), we want to add the number to some total 
(that starts at zero, of course) and we'll increment some counter (which 
also starts at zero) by one.  When we've run out of numbers (i.e., we hit 
the end of the list), we want to return the total divided by the counter 
to give us our average. Here's what it looks like in Scheme:

 (define (average numlist)   ;; do list: for every element
                             ;; of a list do this...
   (do ((sum 0)              ;; initialize variables
        (items 0))
     ((null? numlist) (* 1.0 (/ sum items))) ;; test for
                                             ;; list end
     (set! sum (+ sum (car numlist)))        ;; update
     (set! items (+ items 1))                ;; update
     (set! numlist (cdr numlist))))          ;; reduce
                                             ;; problem

In case you want to see the "streamlined" version with all the work 
done in the preamble, here it is:

 (define (average numlist)
     (do ((sum 0 (+ sum (car listcopy)))
          (items 0 (+  items 1))
          (listcopy numlist (cdr listcopy)))
       ((null? listcopy) (* 1.0 (/ sum items)))))


V. Variation on an average theme

Here's an example which sheds some light on why you might not want to put 
all your work in the do preamble. Let's say you have a list of positive and 
negative numbers. You want to average the positive (or better yet, 
non-negative) numbers and ignore the negative numbers. If I haven't
buried the work in the preamble, the solution is pretty obvious:

 (define (average-non-neg numlist)
     (do ((sum 0)
          (items 0))
       ((null? numlist) (* 1.0 (/ sum items)))
       (if (>= (car numlist) 0)
           (begin (set! sum (+ sum (car numlist)))
                  (set! items (+ items 1))))
                  (set! numlist (cdr numlist))))


(That "begin" thing is something new. It allows you to treat a sequence of
expressions as a single expression. It can come in handy at times.  You should
look it up in your online reference manual that comes with Dr. Scheme.) When
the updating is buried in the preamble, then things can get a bit ugly:

 (define (average-non-neg numlist)
     (do ((sum 0 (if (>= (car numlist) 0)
                     (+ sum (car numlist))
                     sum))
          (items 0 (if (>= (car numlist) 0)
                       (+ items 1)
                       items))
          (numlist numlist (cdr numlist)))
       ((null? numlist) (* 1.0 (/ sum items)))))


But I guess beauty is in the eye of the beholder, no?


VI. Scheme iteration versus iteration in other languages

Scheme's "do" form imposes lots of constraints on how you can do 
iteration. Unlike in some programming languages, Scheme only lets you 
test for termination at the top of the loop (sometimes called a 
"pretest loop" or "sentinel loop"). Other languages let you test at 
the bottom of the loop (a "posttest loop") as well as the top, and 
still others let you put as many tests as you want anywhere in the 
loop. (In general, multiple exit points from a loop is considered bad 
style, so avoid this even if your language allows it. It can be very 
confusing.)  However, it has been proven that all loops can be 
rewritten as pretest loops, so this constraint isn't so bad.

Similarly, other languages provide additional control structures like "do while" (some tested condition is true) or "do until" (some tested condition is true). These may be more readable and easier to use than a generic "do" structure...actually, they ARE more readable and easier to use...but they can be implemented in Scheme's "do" form. In your programming career, if you have one, you'll see lots of different ways of making loops happen; just remember that they're all just variations on the same theme. VII. Iteration versus recursion So when do you use iteration and when do you use recursion? Sometimes the choice is obvious. For example, in the case of factorial, the mathematical solution is defined recursively, so it makes sense to write it up recursively, no? Otherwise, you get something not so obvious (define (factorial n) (do ((result 1 (* result counter)) (counter n (- counter 1))) ((= counter 0) result))) instead of something very obvious that maps directly from the mathematical definition: (define (factorial n) (if (= n 0) 1 (* n (factorial (- n 1))))) Other times, the way the problem data is represented (i.e., the data structures being used) will point to whether a recursive or iterative solution is better. Often, a recursive approach offers a conceptuatlly cleaner or more elegant solution to a problem than an iterative alternative, but sometimes at the expense of computational resources. For example, when you need to traverse hierarchical linked list structures (lists, trees, graphs, networks), you can often write fairly small and understandable solutions to seemingly complicated problems. Recursion takes care of a lot of the "bookkeeping" for you by storing things on the activation stack, and if you don't think that makes your life easier, try doing any of your graph search or state space search homework problems using iteration instead of recursion (but don't turn it in that way). Then take a couple of Tylenol. Heck, take the whole bottle. When you do a lot of recursion, however, there's the added expense of memory usage and time taken to push stuff on the stack. On the other hand, we know that we can circumvent that expense by translating to a tail-recursive solution, and there are even compilers that will optimize recursion (tail recursion for sure, and sometimes even augmenting recursion) for us so that what the computer is actually doing is executing a loop without stack usage even though it looks like recursion from the outside. (I'm told that the GNU C compiler even optimizes tail recursion.) In short, recursion isn't really the resource waster that many folks think it is. So if recursion is so hot, why bother to learn iteration at all? Well, there are other large classes of problems that may be more amenable to an iterative solution, again depending on how the problem data is organized. In short, some data structures, especially those that mimic the underlying architecture of the computer itself, are especially suited for use with iterative approaches. We'll look at some of those data structures real soon now. VIII. Waving goodbye to functional programming...sort of Remember way back at the beginning of the semester when we talked about the differences between the various programming paradigms?: "When you're working with computers, you use programming languages to create the programs that spawn the computational processes whose study is the heart and soul of computer science. Which programming language you should use depends on all sorts of factors. For now, it's sufficient to know that there are all sorts of different programming languages, and they come in all sorts of different flavors. Historically, most languages have been of the "procedural" flavor (also known as "imperative" languages). These languages, like BASIC, or C, or FORTRAN, or Pascal, tend to reflect the low-level behavior of the computer itself. Hence these languages sort of force the computer programmer to cast his or her solutions to a programming problem in terms of the low-level operations of the computer." "Another programming language flavor (these flavors are often referred to as "programming paradigms") is comprised of the "object-oriented" programming languages, like C++, Java, and Smalltalk. As opposed to the procedural languages, the object-oriented languages encourage or require the programmer to think about procedures and the data those procedures manipulate at the same time. This in turn results (we hope) in better designed programs that aren't so closely tied to the low-level computer operations." "A third programming paradigm is the functional programming paradigm. In this paradigm, we view computer programs as collections of procedures that behave much like the mathematical functions you've been working with in your math classes for years." That stuff in quotes is from the notes for the second lecture, way back in August. Well, recently we've been departing from the functional paradigm, as you know. We've introduced things like variables and assignment, the notions of side effects and persistence (which are essentially two different ways of saying the same thing), and sequences of instructions that are useful only when there are side effects. The direction we're heading right now is toward that procedural paradigm mentioned above, in which we map our problems and their solutions onto the underlying operations of our friendly digital computer, instead of mapping them onto mathematically-inspired functions. One of the things you may notice is that as we move into the procedural programming paradigm, things are going to get more complicated. You're going to have to keep better track of what's going on, because you're stepping beyond the "protections" that functional programming provides for programmers. Furthermore, that nice substitution model of evaluation that we used for tracing the execution of our functional programs no longer applies; for example, when you use looping instead of recursion for repetition, there aren't any substitutions. The procedural paradigm goes by different names. It's sometimes called the imperative programming paradigm, as the instructions are usually imperatives to the computer to retrieve, compute, and store information. And because the storing of that information enables the next instruction or imperative in some sequence to use that stored information and compute something else of value, this paradigm is sometimes called the sequential programming paradigm. Whatever the name, the paradigm is tied closely to the underlying organization (or "architecture") of the digital computer. The organization of a digital computer, at its highest, hand-wavy levels, isn't too awfully complicated. There's a central processing unit (CPU) that controls everything that's going on, there's a big ol' chunk of memory (known as RAM for random access memory) that's used to store things like programs and data, there's a small chunk of very specialized memory called registers that are the only places that the CPU can use to manipulate data (like add two numbers with the help of another component called the arithmetic logic unit, or retrieve some value from memory, or store some value back into memory, or retrieve the next instruction to be executed, and so on), and there are pathways that enable information to move from memory to registers and back...those pathways are called busses. Chapter 13 of your textbook has a nice description of how all this stuff works, as part of a discussion about how to build compilers and interpreters. You should read it if you have time and interest. We won't hold you responsible for contents of that chapter, so don't worry about that. But you should take this opportunity to get familiar with how your computer works, because they all work the same, whether you have a Dell, or an IBM, or a Compaq, or an Apple, or a Sun. IX. Vectors Most programming languages provide one or more data types that mimic the behavior of computer memory. Memory consists of a bunch of cells organized in linear fashion, and each cell has a unique address that distinguishes it from other cells. The CPU, when it wants to retrieve a value from a specific cell, simply specifies the appropriate address (via an "address register"), sends a signal to memory, and memory gives a copy of the contents of to the CPU (via a "data register" or "contents register"). Note that what was stored in the cell being referenced is still there. On the other hand, when the CPU wants to store something in a cell in memory, it puts the value to be stored in the data register, puts the address of the memory cell in the address register, and sends a different signal to memory, which then copies the value from the data register and writes it into the desired cell, destroying what was in that cell before. We could model that memory stuff using association lists, but it would be ugly. We could use the assoc function to retrieve a value associated with a pretend address, but actually locating that item in the association list takes O(n) time, because assoc has to look at each element in the association list to find what it's looking for. On the other hand, real computer memory retrieves a value from an address in O(1) time (i.e., constant time), regardless of the size of the memory. It doesn't get much better than that. Even worse, if you wanted to "write" a new value into a "memory cell" in our association list simulation of memory, you would have to do a lot of list construction...you couldn't just say "now put this thing back where you got it". But real computer memory allows you to do exactly that...you can just say "put this value in that slot" and it will happen in constant time again, regardless of the size of the memory. No consing necessary. Fortunately for us, Scheme provides us with a data type that works a whole lot more like computer memory than association lists do. That data type is called a vector. A vector is a linear data structure of arbitrary fixed size whose elements are indexed by (or associated with) the integers beginning with 0. In some ways, a vector is like a list. It's a linear collection of elements, and the elements in a Scheme vector can be of any data type. (Note that in many other languages, vectors or their equivalent are homogeneous data structures in that all elements must be of the same data type. Scheme tends to be an exeception in that regard, always looking to make your programming life a little bit easier.) But a vector is not a linked list. It's more like a contiguous block of memory cells, where the relationship between individual cells is determined by position within the vector, not by links between elements. Because there's no need to store link information, vectors tend to take up less space than linked lists. Also, vectors have fixed size. You can't just keep adding elements like you can with lists. And because vectors are a whole lot like computer memory, you can retrieve individual elements of a vector very quickly just by passing the appropriate integer index to the appropriate retrieval function. Similarly, you can write a new value into a location in the vector just by passing the appropriate index to the appropriate update function. In other words, you can change a value in a vector data structure just by saying "change that value"...you don't have to rebuild the whole structure just to change one element. Functional programming people like to call that "mutation"...the ability to change a data object without building a new copy...and a vector is a "mutable data object". We'll just lump it all under the heading of side effects. And by the way, some of you might be thinking "hmmm, this vector thing is a lot like an array." You got it right...that's exactly what a vector is: a one-dimensional array. X. Playing with vectors Let's dive right in and see what sorts of things Scheme provides us in vector land. We fire up DrScheme, and at the top level of the evaluation window, we start off by creating a five-element vector with some numbers built into it (they could be your quiz scores again): Welcome to DrSchemeMzScheme. > (define x (vector 10 100 90 50 45)) What this says is make a five-element vector, and here are the five elements. We bound that new vector to the symbol x, so we look to see what x is bound to now: > x #5(10 100 90 50 45) And that's how Scheme prints vector contents. It may look like a list, but don't be fooled...it's not a list, and you can't find the car of a vector. > (car x) car: expects argument of type ; given #5(10 100 90 50 45) The #5 at the beginning of what was printed tells you that the vector has five elements. What's in the parentheses are the values of those elements. There are other ways of creating vectors. You could do this: > (define y (make-vector 6)) > y #6(0) That says make a six-element vector, and I don't care what the elements are just yet (but DrScheme will set them all to zero for you if you don't specify otherwise). Oh, Scheme is lazy when it comes to printing vector contents, so even though y is a six-element vector (noted by the #6), all the values are the same so Scheme just prints one zero. > (define y (make-vector 6 'foo)) > y #6(foo) The only difference between this make-vector call and the previous one is that we told Scheme how to initialize the elements. And note that every time we created a vector, we always had to state in advance how many elements were in the vector, either by giving the elements themselves or giving the size. We can't change the size of a vector without creating a whole new vector. Now let's say we want to retrieve some element from vector x. We just use the vector-ref call, and pass it the vector and the integer index (remember, if we have a five-element vector, and the indices always start at 0, then the legal indices are 0, 1, 2, 3, and 4. There is no index of 5 in a five-element vector. > (vector-ref x 2) 90 What if we want to figure out the number of elements in a vector? We can't use "length", and we can't use recursion to cdr down the vector and count each element. Scheme gives us a length function for vectors: > (vector-length x) 5 And if we want to see if some data object is a vector? > (vector? x) #t > (vector? '(a b c)) #f Watch out, here come the mutants! No wait, that should be mutators. If you want to change the value of an element in a vector, you call vector-set! (Note that exclamation point...operators that have side effects typically get an exclamation point after their names, just like predicates get question marks. It's a Scheme thing.) > (vector-set! x 4 65) Here I just said change the last (fourth) element of vector x to the number 65 (it used to be 45...I guess somebody's quiz was regraded). Let's take a look at x to see if it worked: > x #5(10 100 90 50 65) Sure enough. Now let's see if I can put something other than numbers into vector x...is it really a heterogeneous data structure? > (vector-set! x 2 '(foo bar)) > x #5(10 100 (foo bar) 50 65) Yes it is! However, if I just wrote over somebody's quiz score with the list (foo bar), they're not going to be happy. Oh well. And finally, what happens when I try to use an index that's outside the range of legal indices for my vector? > (vector-ref x 5) vector-ref: index 5 out of range [0, 4] for vector: #5(10 100 (foo bar) 50 65) XI. Iteration and vectors Earlier I said that iteration was a natural choice to use for repetition with some data structures. As you might have guessed, one of those data structures is the vector. Here, just for the sake of comparison, and to allow us a little bit of code reuse, is the iterative version of averaging a list of quiz scores: (define (average numlist) (do ((sum 0) (items 0)) ((null? numlist) (* 1.0 (/ sum items))) (set! sum (+ sum (car numlist))) (set! items (+ items 1)) (set! numlist (cdr numlist)))) If we had those numbers in a vector instead of a list > (define x (vector 10 100 90 50 45)) I could use this procedure to index through my vector of quiz scores, add 'em all up, and divide by the number of scores (which is given by vector-length): (define (average numvector) (do ((sum 0) (index 0) (items (vector-length numvector))) ((> index (- items 1)) (* 1.0 (/ sum items))) (set! sum (+ sum (vector-ref numvector index))) (set! index (+ index 1)))) Welcome to DrScheme, version 100alpha4. Language: MzScheme. > (average x) 59.0 > But what if I still want to use recursion, even though I'm working with arrays? You could do this: (define (average-recursive numvector) (average-helper numvector 0 0 (vector-length numvector))) (define (average-helper numvector sum index items) (cond ((> index (- items 1)) (* 1.0 (/ sum items))) (else (average-helper numvector (+ sum (vector-ref numvector index)) (+ index 1) items)))) But even I have to concede that the recursive version is looking somewhat less elegant than the iterative version. Copyright (c) 2003 by Kurt Eiselt. All rights reserved, with the exception of stuff that belongs to somebody else.

Last revised: November 4, 2003