Defining Your Own Scheme ProceduresSummary: We explore why and how you might define your own
procedures in Scheme.
A Problem: Repeated Computations
In previous labs, you've seen that many algorithms
require more than just one or two procedure calls.
For example, in the lab on turtle
graphics, we saw a series of instructions that created
either a pentagon or a five-side star, depending on the value of
angle.
How do we use this code to draw a pentagon? We write an instruction
to set angle to 72 and follow that instruction with the
instructions to draw the five lines.
(define angle 72)
Clearly, that's a bit cumbersome to write every time we want a pentagon.
What we'd really like to do is to say is that this group
of code is a procedure that takes an angle as an
input and builds a five-sided figure.
You know that Scheme has procedures, since
you've used both built-in procedures -- such as
sqrt,
and * -- and MediaScript extensions, such as
turtle-new and
image-stroke-selection!. But can you
define your own procedures? Certainly.Defining Procedures
In Scheme, you use define to give names to procedures, just
as you use it to give names for values. The values just look different.
The general form of a procedure definition is
(define procedure-name
(lambda (formal-parameters)
body))
The procedure-name part is obvious: It's the
name we might give the procedure. The formal-parameters
are the names that we give to the inputs. For example, the input to a
draw a shape with five lines procedure would probably
be the angle between consecutive lines. The body
is the expression (or sequence of expressions) that do the computation.
A Procedure to Draw Pentagons, Five-Pointed Stars, and More
For the turtle code above, we might choose the name
turtle-penta-draw!. As we just
suggested, one parameter to that procedure will be the angle between
consecutive lines. Do we need any others? Yes! We also need
to specify which turtle should draw the lines. By convention, the
turtle will be the first parameter, which means that the angle should
be the second parameter. Do we need others? Well, it may be the
case that not every five-sided figure should have a side length of 50,
so we'd make that a parameter, too. What should the body look like?
A lot like the code above.
Putting it all together, we get
This brand new procedure can now be called as it were a built-in
procedure. Hence, to have a turtle named tommy draw a
pentagon with each edge of length 80, we might write
>(turtle-penta-draw! tommy 72 80)A Square Procedure
The turtle-penta-draw! procedure
is called for its side effect of drawing on
the screen. (We end the name with an exclamation point to reminder
ourselves that it has a side effect.) However, just as often, we
write procedures that return newly computed
values. And we can write procedures that can compute anything we
know how to write an expression for. For example, here is a simple
square procedure.
(define square
(lambda (n)
(* n n)))
We can (and should) test the procedure.
>(square 2)4>(square -4)16>(square square)Error: *: argument 1 must be: number.Another Example: Grading Homework
The square computation is fairly simple. We can, of course, write
more complex expressions. For example, consider the problem of
generating an average numeric grade, given a six grades (on exams,
quizzes, whatever). A pure average would simply add the six numbers
together and divide by six.
(define compute-grade
(lambda (grade1 grade2 grade3 grade4 grade5 grade6)
(/ (+ grade1 grade2 grade3 grade4 grade5 grade6 )
6)))
A more generous policy for dealing with such grades
would be to drop the lowest grade and count the highest grade twice.
We might express that policy as
(define compute-grade
(lambda (grade1 grade2 grade3 grade4 grade5 grade6)
(/ (+ grade1 grade2 grade3 grade4 grade5 grade6
(max grade1 grade2 grade3 grade4 grade5 grade6)
(- (min grade1 grade2 grade3 grade4 grade5 grade6)))
6)))
Of course, it would be nice to make this procedure work with a varying
number of homework grades. We'll see one such strategy in a few days,
when we begin to explore lists.
Let's look at another example to think a bit more about how one creates
a parameterized procedures. Often, we start
with code that accomplishes a particular task and think about how
(and why) we might generalize it. As an example, consider the
following variation of
instructions in the lab on drawings
as values. These instruction generating a green circle of diameter
40, centered at (60,100).
What would we change if we wanted the circle to be red?
We'd replace the "green" with "red". What
would we change if we wanted the radius to be 100? We'd replace
the 40 by 200. Similarly, we would change
the 60 and 100 for a different origin. We might, therefore, generalize
the expression by replacing the constant values with variables, as
in
We could now draw the same circle by defining edge-size,
center-row, center-col, and color.
(define radius 20)
(define center-col 60)
(define center-row 100)
(define color "green")
While this code is a little bit longer than the original, it's much
clearer. When we want to draw something different, we can just
change one or more of the definitions. And that's just what we
would have done before learning about procedures. But we can now
encapsulate the code into a procedure.
Documenting Your Procedures
Convention in Scheme (and all programming languages) is that we carefully
document what our procedures do, including input values, output values,
and assumptions. We use comments provide information to the reader of
our program (that is, to people instead of the computer). In Scheme,
comments begin with a semicolon and end with the end of the line.
There are a variety of kinds of comments we write. For now, we'll focus
on the comments we write for the other programmers who might call the
procedures we write.
;;; Samuel A. Rebelsky and Janet Davis
;;; Department of Computer Science
;;; Grinnell College
;;; {rebelsky,davisjan}@grinnell.edu
;;; Procedure:
;;; square
;;; Parameters:
;;; val, a number
;;; Purpose:
;;; Compute val*val
;;; Produces:
;;; result, a number
;;; Preconditions:
;;; val must be a number
;;; Postconditions:
;;; result is the same "type" of number as val (e.g., if
;;; val is an integer, so is result; if val is exact,
;;; so is result).
;;; Citations:
;;; Based on code created by John David Stone dated March 17, 2000
;;; and contained in the Web page
;;; http://www.math.grin.edu/~stone/courses/scheme/procedure-definitions.xhtml
;;; Changes to
;;; Parameter names
;;; Formatting
;;; Comments
(define square
(lambda (value)
(* value value)))
Yes, that's a lot of documentation for very little code. However, it
is better to err on the side of too much documentation than too little
documentation. More importantly, as you start writing more procedures,
their purpose and details will be much less obvious when you come back
to them. Finally, when you carefully document procedures, you begin
to think more carefully about what they really need to do and how you
ensure that they do so for all cases.
Consider again the compute-grade
procedures from above. We can write a common set of documentation for
them:
;;; Procedure:
;;; compute-grades
;;; Parameters:
;;; grade1, a real number
;;; grade2, a real number
;;; grade3, a real number
;;; grade4, a real number
;;; grade5, a real number
;;; grade6, a real number
;;; Purpose:
;;; Compute a weighted average of the six grades, using the
;;; top-secret course grading policy.
;;; Produces:
;;; grade, a number
;;; Preconditions:
;;; All grades must be non-negative.
;;; Postconditions:
;;; grade is no smaller than the smallest of the six grades.
;;; grade is no larger than the largest of the six grades.
;;; grade is non-negative (implied by previous postconditions).
It turns out that writing this documentation helped us think a bit more
about some particular issues that relate to the procedure. For example,
we needed to specify something about the result that the client would
find useful. At the same time, we wanted to keep the documentation
general enough that we could use either policy. That made us decide that
putting limits on the result was appropriate. Those limits are, however
limiting. We cannot, for example, write a procedure that gives as
student who does every assignment a bit of extra credit, since that might
lead to a grade higherthan any given so far.
We also had to specify that the procedure took numbers as inputs and
produced a number. Without such consideration, we might have had an
awkward moment in which someone called our procedure with letter grades,
or expected it to return a letter grade.