Skip to main content

Writing Your Own Procedures

Summary
We explore why and how you might define your own procedures in Scheme.

A Quick Review: RGB Colors, RGB Transformations, and Procedures for Building Procedures

As you may recall, in recent readings and labs we have been exploring the RGB representation of colors and procedures related to that representation. We began with a few simple procedures to create RGB colors, procedures like (irgb r g b) and (color-name->irgb color-name). We then saw that we could extract information from RGB colors, particularly with (irgb-red color), (irgb-green color), and (irgb-blue color).

We then learned about procedures that let us transform colors, building new colors from old. For example, (irgb-redder color) creates a redder version of color and (irgb-rotate color) “rotates” the three components of color. We also saw that we could apply these transformations to images as well as colors, using (image-variant image transformation)

Finally, we learned an important general technique: We can build our own procedures from existing procedures by using (compose procn ... proc1) and (section proc info1 ... infon). We saw, for example, that we could write irgb-redder by judicious use of section and irgb-add.

That’s a wide variety of topics and procedures, a variety that can carry us through into our next steps in learning.

Building Other Functions

Although we can build some of the transformations with clever uses of compose and section, we will find that there are other color transformations that seem more difficult to build using these techniques, even though we could explain how to do them as a series of steps. For example, irgb-rotate is relatively straightforward: It appears to require calling irgb-red, irgb-green, and irgb-blue on the color and then applying irgb to the three results.

> (define color (irgb ...))
> (define rotated-color (irgb (irgb-green color)
                              (irgb-blue color)
                              (irgb-red color)))

Similarly, if we wanted to convert a color to a similar brightness of grey, we might want to average the red, green, and blue components and then use that as the new red, green, and blue components.

> (define color (irgb ...))
> (define grey-component (* 1/3 (+ (irgb-red color) (irgb-green color) (irgb-blue color))
> (define grey-color (irgb grey-component grey-component grey-component))

But neither of these sequences of instructions follows the model of “apply a series of procedures, each to the result of the previous procedure” or “fill in one parameter of a multi-parameter procedure”. In both cases, we want to compute three values and then apply another procedure to those three values (irgb in the first case, + in the second case).

It appears that we need a more general mechanism for defining procedures, one that allows any structure of computation, and not just the two models supported by compose and section.

Defining Procedures

Fortunately, the designers of Scheme gave us another, more general, model. You’ll find that this more general model is relatively straightforward, if not as concise as either composition or sectioning.

Typically, we think about three main aspects of the procedures we write: The name we will use to refer to the procedure, the names of parameters or inputs to the procedures, and the instructions the procedure is to execute.

In defining our own procedures using composition and sectioning, we’ve typically used define to name those procedures. We will continue to do so using the more general model.

In defining our own procedures using composition and sectioning, we’ve typically left the names of the parameters implicit, rather than explicit. For the composition operator, compose, we’re working with unary functions, and so we have an implicit “the only parameter”. For the sectioning operator, we used <> to indicate the position of the parameter, but still did not name it. In the more general model, we will find it necessary to explicitly name parameters.

Here’s the general form of procedure definitions in Scheme. (The indentation is optional, but recommended.)

(define procedure-name
  (lambda (formal-parameters)
    body))

The “define” you’ve already seen; it lets us name things. In this case, we’re naming a procedure. The procedure-name part is obvious: It’s the name we give the procedure. The lambda is a special Scheme keyword for “Hey! This is a procedure!”. (Lambda has a special place in the history of mathematical logic and programming languages, particularly in LISP-like languages. It’s special enough that it’s used as the symbol for DrRacket.) The formal-parameters are the names that we give to the inputs. For example, the input to a procedure that averages three colors would probably be generic names for the three colors (e.g., color1, color2, and color3). The body is the expression (or sequence of expressions) that do the computation.

When you first write procedures, you should strive to make the body of the procedure be a single expression, although perhaps a nested expression. Scheme can do unexpected things when you put multiple expressions in the body of a procedure. In addition, we prefer that you not put define expressions in the body of procedures.

Rotating Color Components

Now that we have a form for writing general procedures, we can try to write the two procedures from earlier in the reading. We’ll start with rotating the components of a color. You may recall that we had a fairly straightforward expression for computing the rotated version.

> (define color (irgb ...))
> (define rotated-color (irgb (irgb-green color)
                              (irgb-blue color)
                              (irgb-red color)))

As we think about turning this into a procedure, we need to choose a name (e.g., my-irgb-rotate), a parameter name (e.g., color) and identify the body. We should be ready to go.

(define my-irgb-rotate
  (lambda (color)
    (irgb (irgb-green color)
          (irgb-blue color)
          (irgb-red color))))
> (irgb->string (irgb-rotate (irgb 1 2 3)))
"2/3/1"
> (irgb->string (my-irgb-rotate (irgb 1 2 3)))
"2/3/1"

It looks like we’ve succeeded! To be sure, let’s define an alternate version that rotates the components in the opposite direction.

(define my-irgb-rotate2
  (lambda (color)
    (irgb (irgb-blue color)
          (irgb-red color)
          (irgb-green color))))
> (irgb->string (my-irgb-rotate2 (irgb 1 2 3)))
"3/1/2"
> (irgb->string (my-irgb-rotate (my-irgb-rotate2 (irgb 1 2 3))))
"1/2/3"

Yup. It looks like it works this way, too.

Converting to Greyscale

Let’s look at the steps we used for converting a color to a similar brightness of grey.

> (define color (irgb ...))
> (define grey-component (* 1/3 (+ (irgb-red color) (irgb-green color) (irgb-blue color))
> (define grey-color (irgb grey-component grey-component grey-component))

These steps are a little more complicated than those we used in rotating colors. In particular, we have two big operations to do: First, we need to compute the average component. Then, we have to build a new color using that average for all three components. We can write this as one big expression.

(define irgb-greyscale-1
  (lambda (color)
    (irgb (* 1/3 (+ (irgb-red color) (irgb-green color) (irgb-blue color)))
          (* 1/3 (+ (irgb-red color) (irgb-green color) (irgb-blue color)))
          (* 1/3 (+ (irgb-red color) (irgb-green color) (irgb-blue color))))))

Let’s check it out.

> (irgb->string (irgb-greyscale-1 (irgb 10 20 60)))
"30/30/30"
> (irgb->string (irgb-greyscale-1 (irgb 40 0 5)))
"15/15/15"
> (irgb->string (irgb-greyscale-1 (irgb 0 30 0)))
"10/10/10"

It appears to have worked the way we’d expect. Certainly, the three components are identical, which makes it a shade of grey. In addition, the new components do seem to be the arithmetical average of the three original components.

However, this solution is not especially elegant. In particular, we seem to be repeating the same computation three times, which is both hard to read and inefficient. What can we do instead? We can write additional “helper” procedures to compute the average component and to build a grey color from the one component value.

;;; Procedure:
;;;   irgb-average-component
;;; Parameters:
;;;   color, an integer-encoded RGB color with components r, g, and b.
;;; Purpose:
;;;   Compute the average of the three components in color
;;; Produces:
;;;   ave-component, a non-negative rational number
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   ave-component = (* 1/3 (+ r g b))
(define irgb-average-component
  (lambda (color)
    (* 1/3 (+ (irgb-red color) (irgb-green color) (irgb-blue color)))))

;;; Procedure:
;;;   irgb-grey
;;; Parameters:
;;;   brightness, a non-negative rational number
;;; Purpose:
;;;   Create a grey color of the appropriate brightness
;;; Produces:
;;;   grey, an integer-encoded RGB color
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   The three components of grey are equal.  That is,
;;;     (irgb-red grey) = (irgb-green grey) = (irgb-blue grey)
;;;   All three components are close to brightness.  That is
;;;     (abs (- (irgb-red grey) brightness)) < 1
(define irgb-grey
  (lambda (brightness)
    (irgb brightness brightness brightness)))

(define irgb-greyscale-2 
  (lambda (color)
    (irgb-grey (irgb-average-component color))))

You’ll note that we added a lot of comments to the new procedures we wrote. As you’ll see in a few days, it’s good practice to provide careful documentation for the procedures you write. We will often, but not always, include documentation for the sample procedures we provide for you.

Now, let’s see how well these new procedures work.

> (irgb-average-component (irgb 0 10 50))
20
> (irgb->string (irgb-grey 111))
"111/111/111"
> (irgb-average-component (irgb 70 70 80))
73 1/3
> (irgb->string (irgb-grey (irgb-average-component (irgb 70 70 80))))
"73/73/73"
> (irgb->string (irgb-greyscale-2 (irgb 0 60 0)))
"20/20/20"
> (irgb->string (irgb-greyscale-2 (irgb 20 0 60)))
"26/26/26"

Looks good. When we had non-integer averages, it looks like it rounded to the nearest integer.

Are we done? Not quite. If you look back at the definition of irgb-greyscale-2, you’ll see that all it’s doing is applying irgb-average-component to a color, and then applying irgb-grey to the result. That suggests that we can just use composition.

(define irgb-greyscale-3 (compose irgb-grey irgb-average-component))

Checking whether this works as well is left as an exercise for the reader.

Computing with Numbers

While the procedures above are intended to build colors, we certainly write procedures to deal with other kinds of values. In particular, we can write procedures that can compute anything we know how to write an expression for. Often, we write procedures to help us with mathematical computation.

For example, here is a simple square procedure that computes the square of a number.

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

We will consider other such procedures when we examine Scheme’s numeric values in more depth.

Self Checks

Preparation

a. Start GIMP and enable the DBus Server. Then start DrRacket.

b. Save a copy of procedures-rgb-lab.rkt, which contains most of the code from the reading.

c. Open the file in DrRacket. Review the file to see what values and procedures are included. (You may find it easiest to look at the list provided by using the DrRacket (define …) menu. Finally, click Run.

If things go wrong, see our instructions for running the CSC 151 software. Ask for help if you need it.

Check 1: Active Reading

a. Verify that square correctly squares the numbers 5, 10, -3, 1.2, and 0.05.

b. Check that my-irgb-rotate behaves as expected on the sample color values provided.

c. Check that the three versions of irgb-greyscale-x behave as expected on the sample color values provided.

d. Using image-show and image-variant, check that the three versions of irgb-greyscale-x behave as expected on the sample image provided.

Check 2: An Inverse Procedure

Write a procedure called invert that takes a number and produce the multiplicative inverse of that number (i.e., one divided by the number). For example:

> (invert 2)
1/2
> (invert 1)
1
> (invert 2.0)
0.5