-- Techniques for Programming in Scheme --
** Part I : General Programming Techniques **
- Evaluating Expressions
The most important skill a programmer must have is the ability to
evaluate every possible expression in the computer programming language.
(Or at least those necessary to do some useful work.) When developing
a program, you should avoid using expressions that won't evaluate
correctly
(or at all). If you don't know how to evaluate an expression, then it's
only proper to assume it will have negative effects, and you should
avoid
using it in your own code.
Similar to speaking in foreign language, if you just put words together,
chances are people will laugh at your ignorance. I would also hate to
be
the one to mutter an insult or threat that may lead to violence. In a
program, scattering symbols about may result in the computer mockingly
report an ERROR to you, or if you are unfortunate, may cause the
computer to crash.
The moral of this is, "only use expressions if you know they will cause
the expected result." Your ability to program will be directly related
to your ability to formulate simple expressions into more complex
expressions, all aimed at a common goal of doing some useful work.
In order to explain to a worker how to do his work, I must use plain
English; in order to explain to a Scheme Interpreter how to do its
work, I must use plain Scheme.
- Designing and Using Procedures
The first steps to writing or using procedures is to define their major
features. Procedures are most clearly characterized by looking at their
parameters, return values, and their implemented tasks. Given two
procedures with different names, and all of those characteristics being
equal, they may as well be the same procedure. (In other words, names
aren't important, it's what they do that counts.)
Everytime you use or define a new procedure, it should be characterized
by:
1) Parameters - What does the procedure use?
2) Work - What does the procedure do (generally) to get to the return
value?
3) Return Value - What value does the procedure return?
If you find a procedure and cannot identify these elements, then using
it could be hazardous! That red button you press could start World War
III!
* Parameters
What do we need to know about the parameters? Like a recipe, we need to
know what goes in to get what we want out. If we add the wrong
ingredients,
it could be poisonous, or putrid! The parameter types should be defined
clearly to avoid using an incorrect data type (if you are asked to
put in a chicken egg, you'd better not make it a ceramic egg!). Also
make sure that you know the proper number of arguments, and their
appropriate
order in the parameter list (putting them in the wrong order is like
swapping
the ingredients in a recipe).
* Work
What do we need to know about the work done? There are many ways at
getting
to the same goal. Some ways are better than others given particular
circumstances. If I want a cake out of mixing the ingredients,
then I want to have a general idea of how it's done. This would also
help
me decide whether or not another procedure may be more appropriate.
* Return Value
What do we need to know about the results? Whenever we do something,
it's
critical that we identify the outcome. If we want to bake a cake, we
don't want a mud pie as a result. Similarly, procedures must have their
results defined so we know if the output is what we need and expect.
If I'm throwing a party, mud pies are probably not going to impress my
friends!
When you write your own procedures, it's always nice to describe what
its characteristics are in the form of a comment. This is invaluable
for the instances when you go back to your code and need to recall
what purpose it serves! My favorite method is shown in the example:
; merge - Merges two given lists into a single list by alternating
; between both lists. In the front of the list, the odd numbered
; elements are from the first list, and the even numbered elements
; are from the second list. In the case of unequal length lists,
; the remaining elements that can't be merged are appened to
; the end.
; PARAMETERS:
; list1 - The first list to be merged with the second.
; list2 - The second list to be merged with the first.
; RETURNS:
; The list representing the values of the two given lists, list1 and
; list2 merged together.
;
(define (merge list1 list2)
(cond ((empty? list1) list2)
((empty? list2) list1)
(cons (car list1)
(cons (car list2)
(merge (cdr list1) (cdr list2)))) ))
** Part II : Examining Code Through Pattern Matching **
- Determining the Result of an Expression
Understanding the results of evaluating an expression is extremely
important. By looking at code, you should be able to determine not only
what the result of evaluating an expression is, but also what type of
result
it needs to have to avoid causing an error. This analysis is
accomplished
by examining
the form of the expression, as shown in my document on "How to form
Scheme
expressions," and by the three characteristics of the involved
procedures.
Starting with a simple example, what if we were given the following
expression?
(list 23 2 'cat (lambda () 5))
What will this expression return when evaluated? First we must notice
that it isn't a special form. Being a normal list expression, we know
that the first element must be a procedure, and the rest are expressions
which will be evaluated to become the actual parameters of the
procedure.
To determine the result of evaluating this procedure, we need to know
the return value characteristic of the procedure. By looking up the
procedure 'list' in Scheme's specification, we find out that it is a
primitive procedure which takes zero or more arguments of any type
and returns a list data type. So what data type should we expect
from evaluating this expression? A list of data.
Will the actual parameters cause any errors in this expression? The
list
procedure takes any type of data, and we find only numbers, symbols,
and procedures as its actual parameters. These fit the acceptance of
any
type of data perfectly.
Okay, what if we are given a homework problem that puts question marks
in a list that will be evaluated, and it looks like this:
(??? 4 '(1 3 6))
Is there any way to tell what the ??? have to represent? Let's see!
Could
it be a special form? It's possible, and we can't tell by just looking
at
this expression. We can see that it is highly unlikely that it is a
special
form by finding out which forms it could fit. The two forms that should
come
to mind are DEFINE and IF ('if' can lack the alternative, but will
return
an unspecified value if the predicate is false). DEFINE can be quickly
rejected by noticing that we'd have to bind something to the number '4'.
This is illegal and would produce an error. What about IF? Although
possible, using IF with a predicate that was a number instead of a #t or
#f value is unlikely. This tells us that we should treat this list as
a normal form.
Given that this list is being evaluated as a normal form, what can we
say
about it? The first element "must" evaluate to a procedure and the
following
elements must evaluate to the actual parameters of the procedure. Now
we
know the ??? must be a procedure, but what are the procedure's
characteristics?
Well, the procedure takes two actual parameters, the first evaluates to a
number and the second to a list of numbers. We don't know if the return
value has to be anything special because it's not used. It's also
impossible
to tell what the procedure must do to the two given actual parameters.
The final conclusion would be that it must be a procedure that takes two
arguments (we can't define what the arguments "have to be" since we
don't
know how they are used).
- Analyzing Procedures
Being able to determine what a group of expressions do is also
important.
Let's characterize the simple procedure given:
(define (map fn lst)
(if (equal? lst '()) '()
(cons (fn (car lst)) (map fn (cdr lst))) ))
The best way to approach this procedure is by assuming nothing.
This means we should try to determine what data types are going to
be used in the parameter list and return value.
Let's start with determining the data type of the formal parameter 'fn'.
We need to ask ourselves about where and how it is used in the
procedure. The first occurance is in the expression (fn (car lst)).
What can we tell about what the symbol 'fn' must be bound to by
looking at this expression? It's the first element of the list to
be evaluated... This means it must evaluate to a procedure!
How many parameters does this procedure require? Since the list has
only one element other than the procedure, it will have one parameter.
Now how about the type of data the parmeter takes? By looking at
the expression which defines the parameter's value, (car lst), we
see that it must be the first element, the car, in a pair represented
by the symbol 'lst'. We cannot tell that it has to be any special
value. We can see that the return value of 'fn' is being stored in
a pair, but we also cannot tell what type of value it will be.
Now let's finish determining the data type of 'lst'. While determining
the type of 'fn', we discovered that 'lst' pointed to a pair with some
element in its car that the procedure 'fn' could take as its parameter.
Looking at the base case predicate expression (equal? lst '()), we
have further evidence that this is a pair that makes up a list.
Observing
the expression (map fn (cdr lst)) shows that (cdr lst) must return a
value that
is the same type as 'lst'. We can now be confident that 'lst' makes up a
list of data
that 'fn' can be applied to. (Note that the cdr of a list is always
another
list, unless the list is an empty list.)
The base case evaluates to an empty list. This result tells us that the
return value is most likely a list of something. To find out what it is
a list of, we must look at the other possible return values. The second
possible return value is created by evalutating
(cons (fn (car lst)) (map fn (cdr lst))). We should notice that the
cons
pair constructor can form lists if the first actual parameter is a value
in the list and the second actual parmater is a list of more values.
The first actual parameter is found by evaluating (fn (car lst)). This
tells us that the elements in the list will be the same type of data as
returned by the procedure 'fn'.
The second actual parameter is formed by evaluating
(map fn (cdr lst)). Considering that we've already determined that this
'map' procedure returns a list of values, this cons must also construct
a list. Our final conclusion is that the 'map' procedure has a return
type of "a list of elements of the type returned by the procedure 'fn'."
What does the procedure do? Let's break it into parts. If it's given
an empty list, the base case, we get an empty list back. We now
continue
on looking at the rest of the cases in order. If it's not an empty
list,
it becomes a list who's first element is the result of applying 'fn'
to the first element of 'lst' and ending with 'map' applied with the
same procedure 'fn' to the rest of 'lst'. Simply, this returns a list
with all of the values of applying 'fn' to each element in 'lst'.