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