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