I. A quick review
So far we've learned about two big things: how to call a function,
and how to define a function. Since there are some functions already
defined in Scheme, we could learn how to call them before we even
learned how to define them. We'll articulate the rules of function
calls and function definitions in detail a little bit later.
Functions are nothing more than collections of Scheme instructions,
and they tend to be fairly small. There's a technical definition of
a function that we'll explore later. For now, we'll just say that
a function is a procedure or set of instructions that returns a single
value to whatever called it. So if a function is called by another
function, it returns a value to the function that called it. If you,
the programmer, call a function directly through Dr. Scheme, then the
function returns a value to Dr. Scheme, which in turn prints the value
on the screen because that's the only way it knows to return the value
to you, the programmer. So even though you saw values representing
the lengths of hypotenuses appearing on the screen last week,
note that nowhere in the hypotenuse function is there an instruction
to print anything. You don't know how to make Scheme print anything
yet, because we haven't told you. There's a difference between
printing a value and returning a value. It's an important difference,
and for now we'll just be talking about returning values in class.
So you get values out of a function when it returns that value. And
how do you know what value a function returns? Simple. A function
always returns the very last thing it computes before it's done. So
a function might compute all sorts of things, but it only returns the
last thing it computes.
Now the question is how do you get values into a function? You do
that by passing values through the parameters when you make the
function call. What's a parameter? It's a place holder for the value
that you want to pass to your function. The real value that gets
passed to the function is often called the argument. So in the case
of the function f(x) = x * x, x is the parameter, and when f(x) is
called with the value 2 passed into the function, that value 2 is the
argument. Also, you should know that there's an alternate vocabulary,
where x is called the formal parameter, and 2 is called the actual
parameter. It's kind of confusing, but it's just something we have
to deal with. We'll try to stick to parameter and argument in this class.
While that function is being computed, the parameter x holds the
value 2. That's the way it always worked in high school math, and it
still holds true today. What's also true is that while values are
passed into a math function through one or more parameters, values
are never passed out of a math function via those parameters; in
functions, parameters are a one-way street. Just like in math
functions, parameters in Scheme functions are placeholders or
pathways for values to be passed to the function. In the example just
above, x is once again the parameter, and 2 is the argument. When the
function is evaluated with the argument 2 passed to it, the function
returns the value 4. It does not pass the value 4 back through the
parameter x. Just like in math, parameters in Scheme are one-way
streets. In fact, the generic term for a parameter in a programming
language that only lets you pass values into a procedure is "in
parameter". That's opposed to "out parameter" which only allows
values to be passed out of a procedure to the calling procedure, or
"in/out parameter" which is a two-way street...Scheme doesn't have
out parameters, and theoretically it doesn't have in/out parameters,
but we'll see later that theory and implementation don't always agree.
Sometimes "in parameters" are also known as "call-by-value" or
"pass-by-value" parameters, but I digress...you don't need to know
that stuff right now. We'll get to that later in the semester.
II. The conditional
Let's pretend. With what you know so far about programming, you might
get a job programming for the federal government, perhaps for the
Social Security Administration. At the very least, you could help
them design a program which, given access to the database containing
personal information associated with every currently valid Social
Security number, could print a summary report listing everyone in
that database. An English-like description (which is often called
pseudocode or pseudolanguage) of this design might look like this:
print-summary-report
is defined as retrieve-the-next-record-in-the-database
print-the-summary-information
retrieve-the-next-record-in-the-database
print-the-summary-information
retrieve-the-next-record-in-the-database
print-the-summary-information
:
:
:
where the dots represent the fact that you'll have to repeat the
retrieve/print pair of instructions explicitly for every person in
your database. Given that the population of the United States is in
the ballpark of 280,000,000 or so, the program you're designing above
will be about a half-billion lines long. That's a pretty hefty
program. And what's worse, every time somebody adds or deletes
someone from the Social Security database, which probably only
happens a few thousand times every day, somebody else is going to
have to add a couple of lines to the program or delete a couple of
lines from the program, as appropriate. If you have more people in the
database than the program accounts for, then you can't print a
complete listing. And if you have fewer people in the database than
the program expects, you'll get blank paper, or gibberish, or maybe
your program will do something really unexpected. As one of my
professors in graduate school used to say, EEYOW!
Another way to approach this report-printing problem would be to find
a means of just repeating one pair of retrieve/print operations over
and over until the program reached the end of the database. But to do
that, you as a programmer would have to know how to tell the computer
to check to see if the end of the database had been reached before it
did that retrieve thing. You'd also have to know how to tell the computer
what to do in the case where the end had been reached (a good idea here
would be to tell the computer to stop) as well as in the case where the
end had not been reached (a good idea here would be to tell the computer
to retrieve some information, then print some information, and then do
that check to see if the end of the database had been encountered).
In the world of computing, that ability to test for the existence or
absence of some condition and then choose to do one thing or another
based on that condition is inherent in a class of instruction called
a "conditional". The idea of a conditional is what makes programmed
computers worth having; without conditionals, computer programs
aren't especially useful. At best, they're big, inflexible, and ugly,
like the report printer that we were designing earlier. And at worst,
you simply can't do what needs to be done. So in short, if someone
hadn't introduced the notion of a conditional--the ability of a
program to take different branches based on the result of some test
(also known as changing the flow of program control)--computing as we
know it today simply wouldn't exist. No MP3 players, no Sony
Playstations, no Internet, nothing.
In Scheme the simplest conditional is called "if". Here's the syntax:
(if *test-expression*
*then-expression*
*else-expression*)
You don't really type things that begin and end with "*". I just
use that notation to indicate that you put something real, like an
expression to be evaluated, in that place.
If the *test-expression* evaluates to #t (which is Scheme for
"true"), then the "if" function returns what the *then-expression*
evaluates to. (Since *then-expression* is an expression, we'd expect
to see a function call there, or maybe a symbol bound to some
value...that sort of thing.) If *test-expression* evaluates to #f
(which is Scheme for "false"), the "if" skips the *then-expression*
and returns what the *else-expression* evaluates to.
(Note: actually, so long as *test-expression* evaluates to anything
other than #f, that's considered to be true and the *then-expression*
will be evaluated next. In other words, in Scheme, #f is "false" and
anything else is "true".)
Some of you may be quite stunned to find out that you could use something
other than 1 and 0 to represent true and false. Rest assured that there's
nothing sacred about 1 and 0. Those are just symbolic representations of
a particular concept. They have the meaning they do only because
people have assigned that meaning to them. The Greek philosophers
were doing logical reasoning with truth and falsehood long before anyone
thought of associating the values 1 and 0 to them. And those of you
who have some experience with Java can attest that true is represented
as true, and false is represented as false. Now that Java is replacing
C++ in the computer science advanced placement test, maybe the
reaction to #t and #f won't be so dramatic in the years to come.
Sometimes students ask if it's ok to leave out the *else-expression*,
as some other languages allow. The answer is that it's legal to do
so, but I wouldn't do it because it's not good programming style.
Whenever you as a programmer have a choice between making your
intentions explicitly known to other people who might look at
your program versus making those other people try to figure out what
you wanted to happen, always choose in favor of being explicit. Those
other folks will thank you for it, and you might even thank yourself
if you have to go back and work on that program after you've
forgotten what you were trying to do. As we'll discuss in the near
future, computer programs are first and foremost a medium for expressing
ideas about computation to other people...getting computers to
understand the programs is secondary.
But here's another, more practical reason to be explicit. If you look
up "if" in your Scheme reference manual (and you all have one...you
can find it by clicking on "help desk" in the "help" menu for Dr.
Scheme, then clicking on "installed manuals", and then clicking on
"Revised(5) Report on the Algorithmic Language Scheme", which is
commonly referred to as R5RS), you'll find that when you write an
"if" expression without that else-expression and the test-expression
evaluates to #f (meaning the the-expression is skipped over), what
that "if" expression actually returns is UNSPECIFIED. That means that
the Scheme language designers have left it up to the people who
implement the Scheme language in various different ways. So how Dr.
Scheme handles this may be different from how, say,
MIT Scheme handles this. If you ignore unspecified stuff or rely on
it happening in a certain implementation-dependent way, your programs
may not work on other people's Scheme systems, or they may not be
understandable to people who learned Scheme on other systems.
Unspecified results are always bad; you should always
avoid them. Not just in this class, but forever.
Knowledge of conditionals isn't much help unless you know what kinds
of tests you can put into *test-expression*. Here are a few
rudimentary arithmetic tests that you can use....
III. Predicates
Scheme provides a set of functions which are designed to execute
useful tests and return "Boolean" (named after the mathematician
George Boole) or true/false values depending on the outcome of the
test. These are called predicates, and we use them all the time as
the tests in our "if" functions. Here are some commonly-used
arithmetic predicates:
(= *expr1* *expr2*) returns #t if *expr1* and *expr2*
evaluate to the same number, and
returns #f otherwise
(< *expr1* *expr2*) returns #t if *expr1* is less than
*expr2*, and returns #f otherwise
(> *expr1* *expr2*) returns #t if *expr1* is greater than
*expr2*, and returns #f otherwise
(<= *expr1* *expr2*) returns #t if *expr1* is less than
or equal to *expr2*, and returns #f
otherwise
(>= *expr1* *expr2*) returns #t if *expr1* is greater than
or equal to *expr2*, and returns #f
otherwise
IV. Using "if" -- a very simple example
Let's get a little practice with "if" to get comfortable with how
it looks and how it works. Say we wanted to create a function that
takes two numbers as arguments and returns the larger of the two.
That function already exists in Scheme---it's called "max", but
we'll write our own version with a different name. So this function
will check to see if the first number is bigger than the second number
and, if so, will return the second number. If not, then the second
number is returned. It looks like this:
(define (maximum num1 num2)
(if (> num1 num2)
num1
num2))
Once you've mastered this function, you've mastered "if". There's
nothing to it. Type it in and try it.
V. Using "if" -- a slightly more complicated example
We started down this current pathway by talking about the problem
of having the same little bit of program executed over and over
again. Since we don't know how to retrieve records from files or
print them on printers, let's move on to a simple problem from
mathematics that you've seen before which also just happens to
rely on the ability to do the same thing repeatedly.
Let's say we want to define a function which returns the factorial of
a given non-negative integer. Remember, again from high school math
classes, that the factorial of a non-negative integer can be defined
like this:
n! = 1 if n = 0
n * (n-1)! if n > 0
How will we design our factorial function? Let's start with an
English-language description of what's going on, employing some
functional notation along the way (and just to keep things simple for
now, we won't worry what happens if n is negative or not an
integer...we'll just assume that the argument being passed to
factorial is valid):
factorial (n) is defined as
1 if n = 0
n * factorial (n-1) if n > 0
Now what? Well, we know the whole thing has to start with a left
parenthesis and end with a right parenthesis, no matter what:
(factorial (n) is defined as
1 if n = 0
n * factorial (n-1) if n > 0 )
And we know that we start a function definition with "define" and
we know that the function name and the parameter all belong inside
a pair of parentheses:
(define (factorial n) is defined as
1 if n = 0
n * factorial (n-1) if n > 0 )
And we know that "define" is Scheme's equivalent of "is defined as", so
we don't need that junk anymore:
(define (factorial n)
1 if n = 0
n * factorial (n-1) if n > 0 )
That takes care of the define and parameter parts. All that's left
is the stuff in the middle. All we need to
do now is figure out how to translate this part into Scheme:
1 if n = 0
n * factorial (n-1) if n > 0
It's the body of the function, and it looks like a conditional, so
at the very least we know there are going to have to be some
parentheses surrounding it.
(1 if n = 0
n * factorial (n-1) if n > 0 )
Note that I might do these things in a different order than you would;
that's ok. Eventually we'll get it all done. First, I see a call to the
factorial function that's not quite right...it's missing some
surrounding parentheses. This would be better:
(1 if n = 0
n * (factorial (n-1)) if n > 0 )
And for Scheme, I need to change that call to the "minus" function so
that it has the right syntax too:
(1 if n = 0
n * (factorial (- n 1)) if n > 0 )
And since the "multiply" function call needs the same syntax as
"minus", I need to make this fix:
(1 if n = 0
(* n (factorial (- n 1))) if n > 0 )
So what this says is that I want my factorial function to return the
value 1 if n is 0, and I want it to compute and return the value (* n
(factorial (- n 1))) if n is greater than 0. There's a conditional
expression in there, but it's not yet translated into the Scheme
syntax we want. Based on what we said earlier about the syntax for if
in Scheme, the function body would look more like this:
(if n = 0
then somehow return 1
else somehow return (* n (factorial (- n 1))) )
It's still not quite right. First, what happened to the "if n > 0"
part? Well, if we're assuming that n is a non-negative integer in the
first place, and we've already checked to see if n is 0, then all
that's left is n being greater than 0. So the "if n > 0" part is
implicit in n not being equal to 0.
And then we still haven't completely used the correct syntax for the
"if" form. The test for "n = 0" we now know should use the "="
predicate function, and the form would look like this:
(if (= n 0)
then somehow return 1
else somehow return (* n (factorial (- n 1))) )
And to return the value 1 if n is 0, we don't have to say "then
somehow return 1", we can just say "1" where the *then-expression* is
supposed to be:
(if (= n 0)
1
else somehow return (* n (factorial (- n 1))) )
Nor do we have to say "else somehow return (* n (factorial (- n
1)))"; we just put the expression we want evaluated where the
*else-expression* goes:
(if (= n 0)
1
(* n (factorial (- n 1))) )
So now that whole factorial function should look like this in Scheme:
(define (factorial n)
(if (= n 0)
1
(* n (factorial (- n 1))) ))
What are you waiting for? Type it in and try it out:
> (factorial 4)
24
> (factorial 20)
2432902008176640000
> (factorial 40)
815915283247897734345611269596115894272000000000
> (factorial 60)
832098711274139014427634118322336438075417260636124595244927696409600
000000000000
VI. Recursion
In the discussion above, I glossed over something that's really
important. It's called recursion. I didn't want to scare you, so I
didn't even mention the concept. But this concept, and it's a huge
concept in computer science, does frighten lots of people. In fact,
some of the instructors in the old pseudolanguage versions of this
course actually introduced this concept to their students while saying
"this is the hardest concept to learn in this whole course, and if you
can master this, you can master anything." Ignore all that...it's not
that hard. It's just different, and different is only scary if you let it
be scary. (For proof that "different" can be good if you let it, go
read Dr. Seuss's "Green Eggs and Ham"...trust me, I have a three-year-old
daughter, and we both now know this to be true.)
"Recursion" essentially means defining something in terms of itself.
A function is recursive if it (directly or indirectly) calls itself.
The factorial function above calls itself...that's part of the
definition of factorial, so it just naturally shows up when we
translate that definition into Scheme. You probably didn't freak out
when you first saw the factorial definition in some high school math
class, and you shouldn't freak out now. Nothing has changed. It's
all still just algebra.
Sometimes the following question comes up: How can the factorial
function call itself before it's even completely defined? A good
question. Keep in mind that even though the definition for factorial
contains a call to itself, that self-referential call to factorial
isn't actually executed or evaluated until the function is completely
defined and stored away in the special place where function bodies
are stored with their associated names. (The special place is called
a "symbol table", and other things are stored there too. More about
that in the weeks to come.) So if you type the factorial function
definition from last Thursday into your Dr. Scheme editor window, then
click the "execute" button, Dr. Scheme will "bind" that function body
to the name "factorial", but you haven't yet asked Dr. Scheme to evaluate
a real function call to the factorial function. Then you drop down to
the evaluator window and type something like
> (factorial 3)
and then you see
6
and you can be pretty sure it worked just as planned.
Getting comfortable with recursion sometimes takes a little bit of
faith, and it always takes a lot of practice. Sometimes that practice
can be as simple as testing out factorial with lots of numbers. Try
finding the factorial of 0, then 1, then 2, and so on. Try it with a
big number...say factorial of 100. And if you try it with a number
for which factorial is not defined, such as a number less than 0,
you might encounter the classic kind of error associated with recursion,
the infamous stack overflow. What is stack overflow? We'll talk about
that later too. (Or, you might not encounter the stack overflow,
and we'll explain that too in the future.)
VII. Three easy pieces
A recursive function consists of three parts:
1. The termination condition, or when to stop (a.k.a. the base
case). The termination condition is often, but not always, the first
thing done on entering a recursive function
2. The operation or modification, or what to do to the data to
move closer to a termination condition (a.k.a. the reduction step)
3. The recursive call itself.
If we go back and look at our factorial function, we can easily pick
out the three necessary components for recursion:
(define (factorial n)
(if (= n 0) ; here's the termination condition
1 ; and part of that is what to return
; when done
(* n (factorial (- n 1))) )) ; combined here is
; the recursive
; call and the
; reduction step
Recursion is a program control mechanism that allows repetitive
operations without traditional iteration, which requires the use of
side effects and the maintenance of variables as counters or
temporary storage places. Why no side effects, variables, etc.?
All those things add unnecessary complexity to the procedures they
write and that there are simpler ways to write those procedures using
recursion. Recursion works entirely differently than those other forms
of iteration, and using recursion effectively requires a different
style of thinking than what many students might be used to, but
they'll get better at it with practice. Recursion also results in
nice, clean, compact source code which is often easier to read than
the iterative equivalents.
Another important aspect of recursion, especially for 1321 students,
is that recursion is the only mechanism that allows you to get
repetitive behavior from your functions in a purely functional
programming paradigm. Since you'll be living in the functional
paradigm for awhile, you'll be getting lots of exposure to recursion.
A recursive function can also eat up lots of memory as it is running,
but it doesn't necessarily have to; we'll see more about this later.
Copyright (c) 2003 by Kurt Eiselt. All rights reserved, with
the exception of stuff that belongs to somebody else.
Last revised: August 28, 2003