CS 1321X - Lecture 3 - August 26, 2003

CS 1321X - Lecture 3

Program Control: The Conditional



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