CS 1321X - Lecture 2 - August 21, 2002

CS 1321X - Lecture 2

Fun with Functions



I. An extremely brief overview of programming language concepts

Today we begin teaching you how to write computer programs that spawn 
those computational processes we talked about last time. 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.  Sometimes that's 
a good thing, but other times the problem and its solution may not 
offer a good mapping to low-level computer behavior. It might be that 
a different problem-solving perspective would be more helpful, hence 
the existence of still other kinds of programming languages.

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 more about the procedures and the 
data those procedures manipulate at the same time, while thinking 
less, if at all, about constraints imposed by computer details. This 
in turn results (we hope) in better designed programs that aren't so 
closely tied to the low-level computer operations. In other words, 
the object-oriented paradigm allows the programmer to think about 
solutions that are perhaps more appropriate to the problem than 
might be available in the procedural paradigm.

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. It's probably the 
simplest of the three paradigms, and it's where we'll start in this 
class. So let's review some of that basic math stuff.


II.  The humble function

You remember this from high school, don't you?

     f(x) = x * x   (or f(x) = x^2 where ^2 denotes a superscript 2)

Well of course you do. So, what's f(2) then?

4? Very good.

And what's f(5)?

25? Excellent.

And then what's f(4729185)?

Why it's 22365190764225, of course. 

The important point here is that you all remember how these function 
thingies work.  In mathematics, a function is a relation that maps a 
set of values called the domain onto a set of values called the range 
in such a way that each value in the domain is paired with one and 
only one value in the range. If somehow the relation paired some value 
in the domain with more than one value in the range, then you wouldn't 
have a function.

In this particular example, we can think of the expression

     f(x) = x * x   (or f(x) = x^2 where ^2 denotes a superscript 2)

as the definition for the function named f. So f is the name of the 
function, and the x within the parentheses is called a parameter. A 
parameter is a just a place holder for the actual values that can be 
passed to the function, like 2 or 5 in this example.

On the right side of the = you see the expression x * x. That part is 
called the function body. The function body describes the operations 
that are performed on the values from the domain that are passed as 
parameters to the function to compute the corresponding values from 
the range.

That = in the middle "binds" or associates the function body on the 
right to the function name and list of parameters (there could be 
more than one, no?) on the left.  The whole thing is called a 
function definition.

Once you've defined a function in math land, you can invoke or call 
the function. The expression f(2) is a function invocation. When the 
function f is invoked or called with the value 2 passed to the 
function through the parameter x, the value 2 is substituted for the 
x's in the function body, and the operations are performed. Another 
way of saying that is that the function body is "applied" to the 
value 2. When the function application part is completed, the 
resulting value 4 (the corresponding value from the range) is 
returned.

As you can see, there's a bunch of terminology that's associated with 
functions. You may not have learned the terminology in your high 
school math classes, but we hope you learned the principles of how 
functions work, even if you didn't have names for everything.

There are a couple of interesting and, as you'll see later, useful 
observations that we can make about these functions.

  Everytime you plug a given value into the parameter, you get 
  the same result. That is, f(2) is always 4 in the example above, no 
  matter how many times you do it or no matter what else is going on in 
  the world. The only thing that affects the behavior of the function 
  is the value passed through the parameter. That sort of 
  predictability is desirable, whether you're a mathematician or a 
  computer scientist.

  Passing a specific value through the parameter list to a 
  function doesn't change the value. That is, calling f(2) didn't 
  change the value 2 to something else, like 4. The value that is 
  passed through the parameter is not altered in any way by the 
  function application mechanism. It continues to be 2. The function 
  doesn't alter anything in the world.

Another thing you might remember about functions is that they're 
composable. That is, you can invoke a function on the result of 
another function call. Or in still other words, you can pass a 
function call as a parameter to another function. For example, here's 
our example function again:

      f(x) = x * x   (or f(x) = x^2 where ^2 denotes a superscript 2)

We know that f(2) returns the value 4. What does f(f(2)) return? The 
"inner" f(2) returns 4, and that value is then passed as through the 
parameter to the "outer" function call, which should then return the 
value 16 (which is 4 * 4) if everything works according to plan.

So that's our little review of all the important math you've learned 
over the past several years. You probably never had this much 
discussion of functions before, so the terminology may be new to you, 
but you are familiar with the concepts, and the terminology will be 
helpful when we start talking about the programming language
Scheme right about now.


III. Scheme

If you had a textbook to look at, or if you paid attention in class 
on Tuesday, you know by now that the language you'll be using 
primarily in this class is called Scheme. Scheme was selected for 
this course by a faculty committee that compared the advantages and 
disadvantages of several languages, including Java, C, C++, Matlab, 
Visual Basic, and even pseudolanguage.  Scheme won out primarily 
for several reasons:

  the syntax (the rules about the structure of an expression, 
  how things are ordered) is very easy to learn and gets students 
  programming very quickly, in part because Scheme is a functional 
  language and students are already familiar with the concept and 
  syntax of functions from their previous math experience,

  it allows us to talk about computation without being too specific
  about the platform 

  it looks like math, so student are already familiar with some
  of the basics

  the language is interpreted so there's quick feedback for new 
  programming students.

  interpreters for Scheme are well developed and available for 
  free for just about every computing platform (i.e., combination of 
  hardware and operating system) out there, and

Uh oh. There's another one of those new words. What does 
"interpreted" mean?  Previously we divided up the world of 
programming languages in terms of the kind of perspective that a 
language provided a programmer when the programmer was considering 
possible solutions to a problem. So we had the world divided into 
procedural, object-oriented, and functional languages. But another 
way to divide up the world of programming languages is on the basis 
of how they're implemented, which in turn dictates how the programmer 
interacts with them. Most languages are "compiled" languages. That 
means that the program you write is then run through another program 
called a compiler, which turns the instructions you've written in 
your chosen language into instructions that can be executed by the 
computer.

But there's more...those instructions have to be processed a couple 
of more times in ways that you'd learn about in CS 2130 if you were 
masochistic enough to sign up for that course, but knowing about that 
extra processing isn't really important to you now. After all that 
processing is done, then and only then can you actually execute the 
program and study the resulting process. That's a lot of work to go 
through to get a program to do something, and it's especially 
distracting for brand new programmers. Examples of compiled languages 
are C, C++, and Pascal.

Other languages are "interpreted" languages. These languages allow 
much more speedy interaction between the programmer and the computer. 
Interpreted languages generally have a more sophisticated interface 
that allows the programmer to type in, for example, just a single 
instruction and have it executed immediately. This sort of interface 
usually makes software development easier for the programmer, which 
is especially beneficial for new programmers, but interpreted 
programs tend to run less efficiently than their compiled 
counterparts. BASIC is a classic example of an interpreted language, 
as are the languages LISP and Scheme. (Please note too that sometimes 
interpreted languages are actually compiled by compilers that behave 
more like interpreters; that's also something that you'd learn about 
in CS 2130. For now, it's sufficient to talk about languages in terms 
of how they behave on the outside, not how they're actually 
implemented on the inside.) Enough of this digression; let's go back 
to talking about Scheme.

Scheme isn't used as widely as some of the languages you may have
been exposed to already, but it (or variants thereof) is used commercially 
(see for example, Yahoo! Store, Orbitz, or Naughty Dog's Crash Bandicoot
or Jak and Daxter).  Some folks wonder if it wouldn't be better if we 
taught Java or C in CS 1321, both of which are very commercial languages. 
It's not our goal in this first course to turn you into professional 
programmers; there are a few other courses you'll have to take before you 
get there. Here we just want to introduce students to the fundamental 
concepts of computing and programming, and that's really easy to do with 
Scheme. And the concepts you learn via Scheme will carry over to other 
languages. Scheme is such a good language for beginners that it's used as 
the introductory language in a growing number of really good computer 
science programs, including Berkeley, MIT, Rice, Yale, and Indiana. 
And in case you were thinking that you might want to program 
computers for a living, we haven't noticed that Berkeley or MIT grads 
are finding it difficult to get jobs or get into grad school because 
they started with Scheme, and we don't think you'll be hurt by it 
either.

Scheme is descended from one of computing's oldest programming 
languages, LISP. LISP has been used extensively in artificial 
intelligence research because of the ease with which programmers can 
manipulate complex data structures containing lots of symbolic 
information (kinda like what's in your head). LISP, like all 
programming languages, has its rabid promoters and detractors, and 
again like most languages is the source of all sorts of religious 
debate. You can participate in these debates in Usenet newsgroups 
like comp.lang.lisp and comp.lang.scheme. Don't tell them we sent you.


IV. From math functions to Scheme functions

We've talked a lot about functions from a math perspective, and we've 
talked a little bit about a programming language called Scheme, with 
the promise that knowledge of functions will help you with Scheme. 
Let's close the loop on that relationship...

The programming language Scheme consists of a handful of basic 
elements. There are numbers, like 3 and -6 (integers), 7.162 and -0.4 
(real numbers), and 1/2 and 15/27 (ratios). There are also symbols 
that are made from alphanumeric characters; symbols look like x or y 
or foo or kurt or b74n.  Symbols are typically used as names. The 
name might be the name of a parameter, or of a function, or of 
something called a variable, which you might remember from 
algebra...we'll get to variables later, but let it suffice to say for 
now that they're not much different from parameters...a little bit 
different, but not much.

Some symbols are pre-defined as names of functions in Scheme. For 
example, the four basic arithmetic operations are already named in 
the language:

    +    is addition
    -    is subtraction
    *    is multiplication
    /    is division

All told there might be on the order of 100 predetermined function 
names in Scheme. That's not a lot when compared to other languages; 
this is another way in which Scheme is a nice language for beginners.

When you first start up the Dr. Scheme programming environment that 
we want you to use (after you download it from the web site that we 
pointed you to), you'll see two windows open up on the monitor screen 
in front of you (assuming of course that you're facing the monitor). 
On the top half of the screen will be an editor window. Ignore that 
for now. The bottom half of the screen will contain an evaluation 
window...that's where the interpreter, also known as the evaluator, 
lives.

(Oh, by the way, when you download and install Dr. Scheme, you'll be
asked to "Please select a language".  You'll want "Standard (R5RS)".
Ignore the other choices.  I forgot to mention that in class today.
That's one reason to read these posted class notes...I'll often add
things that I forgot to mention in class.)

Because Scheme is an interpreted language as opposed to a compiled 
language, we can just start typing things at the evaluator to see 
what happens and get a feel for what Scheme does for us. For example, 
you can type a number after the "prompt symbol" to see what happens:

>  2
2

What happened is that we typed the number 2, the Scheme evaluator 
evaluated what you typed in and said "why, that evaluates to 2" and 
then returns that value to you, since you wanted the number 2 
evaluated.

What if you wanted to invoke or call a predefined function in Scheme? 
Let's try it. Scheme provides us with a function that will return the 
square root of a number. It's called sqrt. In math world, if we had 
defined a function called sqrt, and we wanted to find the square root 
of 4, we'd write

   sqrt(4)  (just like we'd call f(4) if we had named the function f,
                not sqrt)

That's close to how Scheme wants you to call a function, but it's not 
quite right. Because a Scheme interpreter is a tad less intelligent 
than you're average mathematician, Scheme wants more clues about what 
you intend, so it wants you to move that left parenthesis out in 
front of the name of the function that you're calling. In general, 
Scheme will look at a left parenthesis and assume that the very next 
thing that follows will be the name of a function to be invoked. (You 
can circumvent that, but that's for later.) So instead of typing

>  sqrt(4)

and getting an error message, you type

>  (sqrt 4)
2

and all of a sudden this computer stuff is starting to make some sense.

Just like in math world, you can compose those function calls:

>  (sqrt (sqrt 4))
1.4142135623730951


V.  More fun with square roots

Now that you're experts on invoking the sqrt function, you
could get really energetic and use Scheme to compute the 
length of the hypotenuse of a right triangle given the lengths of the 
other two sides. If those two sides have lengths 3 and 4, for 
example, you could type this expression (an expression is anything 
that Scheme could evaluate, like a number, or a function call, or 
some other stuff we'll learn about):

>  (sqrt (+ (* 3 3) (* 4 4)))
5

Why not (3 * 3) instead of (* 3 3)? Remember, Scheme always wants the 
name of the function being called up front. It's called a prefix 
notation. It's "pre" because the name of the function goes first. 
Math notation isn't so consistent; sometimes it's infix notation 
(where the name of the function goes in the middle), sometimes it's 
prefix notation. Mathematicians are smarter than computers, so they 
can figure it out.

You have to admit, that's pretty neat for just a few minutes with the 
programming language. You're already computing hypotenuses. Try 
getting that far in Java or C or some other "commercial" language in 
an hour if you've never done this sort of thing before. It wouldn't 
be quite this easy.  That's one of many reasons we start  with Scheme.

Anyway, what's happened is that the evaluator started by computing 
the square root, but it couldn't do that until it computed the sum 
that was being passed to the sqrt function via its parameter. And it 
couldn't compute the sum until it computed the two products that were 
being passed to the + function via its parameters. Once the products 
9 and 16 were computed, the evaluator could finish computing the sum, 
which is 25, and then finish off by computing the square root of 25 
and returning the value 5.

What if you wanted to compute the length of the hypotenuses of a 
bunch of different right triangles? You could retype that expression 
above over and over, changing the numbers as needed:

>  (sqrt (+ (* 2 2) (* 5 5)))
5.385164807134504
>  (sqrt (+ (* 4 4) (* 3 3)))
5
>  (sqrt (+ (* 4 4) (* 2 2)))
4.47213595499958
>

That's going to get tedious in a hurry. So to make your life easier, 
you could define a hypotenuse function of your own (Scheme doesn't 
already have one) that has two parameters for the lengths of the two 
other sides of the triangle. In math world, that function definition 
might look like this:

      h(a,b) = sqrt( (a * a) + (b * b) )

or

                   ______________
                 /
      h(a,b) = \/  (a * a) + (b * b)


(and note the combination of prefix notation, the square root sign, 
with infix notation for addition and multiplication...like I said, 
mathematicians are smarter than computers).

Turning this into a Scheme function definition isn't very difficult. 
The first thing you need to know about is an operation in Scheme 
named "define". It's what Scheme uses to bind function bodies to 
function names and parameters. So we start by typing this:

      (define

Because we want Scheme to know that we're calling the define 
function. Now we need to give this thing a name. Let's call it 
"hypotenuse" instead of "h"...mathematicians use single letter 
function names, but good computer programmers use bigger names that 
help to explain what's going on. We'll also want a couple of 
parameters, with meaningful names...we'll call them "side1" and 
"side2" for lack of something better.

Scheme expects us to group the function name and any parameters 
inside another pair of parentheses that follows "define":

      (define (hypotenuse side1 side2)

After that, we type in the function body, which is really a function 
call of some sort (which may be in turn composed with another 
function call, and so on, as in this example) written in terms of the 
names of the parameters and the names of functions either already 
defined or soon to be defined. We want that function body to compute 
the square root of the sum of the squares of the sides...that looks 
like this:

(define (hypotenuse side1 side2) (sqrt (+ (* side1 side1) (* side2 side2)))

And then we close the whole thing with a right parenthesis, to 
balance the one in front of "define":

(define (hypotenuse side1 side2) (sqrt (+ (* side1 side1) (* side2 side2))))

If we typed all that following a prompt in the evaluator window, like this:

>  (define (hypotenuse side1 side2) (sqrt (+ (* side1 side1) (* side2 side2))))
>

we'd just get another prompt back. But now we can invoke the function 
we just defined on real values like this:

>  (hypotenuse 3 4)
5
>  (hypotenuse 2 5)
5.385164807134504
>  (hypotenuse 4 3)
5
>  (hypotenuse 4 2)
4.47213595499958
>


Another way to define this function would have been to type the 
function definition in the upper window, the editor window, and then 
just click the "execute" button.  Unless you plan on making no 
mistakes whatsoever, it's a whole lot easier to write your functions 
in the upper editor window...keep editing it up there until it looks 
the way you want it, and then click the execute button, move down to 
the evaluation window and test out your function definition by trying 
out a few function invocations.

Now you're programming in Scheme. It's not really that hard, is it?

Let's end with a style issue.  We don't typically write whole function 
definitions on one line. To aid in readability, good programmers will use 
the vertical dimension as well as the horizontal dimension and spread the 
definition downward. It's something you'll understand after you've seen 
more definitions, but for now just accept that we'd rather see this:

(define (hypotenuse side1 side2)
   (sqrt (+ (* side1 side1) (* side2 side2))))

than this:

(define (hypotenuse side1 side2) (sqrt (+ (* side1 side1) (* side2 side2))))



Copyright (c) 2003 by Kurt Eiselt.  All rights reserved, with 
the exception of stuff that belongs to somebody else.

Last revised: August 22, 2003