Fundamentals of Computer Science I: Media Computing (CS151.01 2008S)

Defining Your Own Scheme Procedures


Summary: 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.

(turtle-forward! tommy 50)
(turtle-turn! tommy angle)
(turtle-forward! tommy 50)
(turtle-turn! tommy angle)
(turtle-forward! tommy 50)
(turtle-turn! tommy angle)
(turtle-forward! tommy 50)
(turtle-turn! tommy angle)
(turtle-forward! tommy 50)

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)
(turtle-forward! tommy 50)
(turtle-turn! tommy angle)
(turtle-forward! tommy 50)
(turtle-turn! tommy angle)
(turtle-forward! tommy 50)
(turtle-turn! tommy angle)
(turtle-forward! tommy 50)
(turtle-turn! tommy angle)
(turtle-forward! tommy 50)

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 DrFu extensions, such as turtle-new and image-stroke!. 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

(define turtle-penta-draw!
  (lambda (turtle angle side)
    (turtle-forward! turtle side)
    (turtle-turn! turtle angle)
    (turtle-forward! turtle side)
    (turtle-turn! turtle angle)
    (turtle-forward! turtle side)
    (turtle-turn! turtle angle)
    (turtle-forward! turtle side)
    (turtle-turn! turtle angle)
    (turtle-forward! turtle side)))

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).

(define sample-circle
 (drawing-recolor
  (drawing-hshift
   (drawing-vshift
    (drawing-scale
     drawing-unit-circle
     40)
    100)
   60)
  "green"))

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

(define general-circle
 (drawing-recolor
  (drawing-hshift
   (drawing-vshift
    (drawing-scale
     drawing-unit-circle
     (* 2 radius))
    center-row)
   center-col) 
 color))

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")
(define general-circle
 (drawing-recolor
  (drawing-hshift
   (drawing-vshift
    (drawing-scale
     drawing-unit-circle
     (* 2 radius))
    center-row)
   center-col) 
 color))

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.

(define drawing-new-circle
  (lambda (color radius center-row center-col)
    (drawing-recolor
     (drawing-hshift
      (drawing-vshift
       (drawing-scale
        drawing-unit-circle
        (* 2 radius))
       center-row)
      center-col) 
    color)))

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.

Creative Commons License

Samuel A. Rebelsky, rebelsky@grinnell.edu

Copyright (c) 2007-8 Janet Davis, Matthew Kluber, and Samuel A. Rebelsky. (Selected materials copyright by John David Stone and Henry Walker and used by permission.)

This material is based upon work partially supported by the National Science Foundation under Grant No. CCLI-0633090. Any opinions, findings, and conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the National Science Foundation.

This work is licensed under a Creative Commons Attribution-NonCommercial 2.5 License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/2.5/ or send a letter to Creative Commons, 543 Howard Street, 5th Floor, San Francisco, California, 94105, USA.