This reading was newly written for Fall 2015. Some issues may remain.
Summary: We explore why and how you might define your own procedures in Scheme.
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
(
and irgb r
g b)(. We then
saw that we could extract information from RGB colors,
particularly with color-name->irgb
color-name)(,
irgb-red
color)(,
and irgb-green
color)(.
irgb-blue
color)
We then learned about procedures that let us
transform colors, building new colors
from old. For example, ( creates a redder
version of irgb-redder
color)
and color( “rotates” the three
components of irgb-rotate
color). We also
saw that we could apply these transformations to images as well
as colors, using color(
image-variant
image transformation)
Finally, we learned an important general technique:
We can build our own procedures from existing
procedures by using ( and
compose
procn
... proc1)(.
We saw, for example, that we could write
section proc
info1 ...
infon) by judicious
use of irgb-redder and
section.
irgb-add
That's a wide variety of topics and procedures, a variety that can carry us through into our next steps in learning.
Although we can build some of the transformations with
clever uses of and
compose, 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, sectionirgb-rotate
is relatively straightforward: It appears to require
calling ,
irgb-red, and
irgb-green on the color and then
applying irgb-blue to the three results.
irgb
>(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
( in the first case,
irgb 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 and
compose.
section
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,
, we're working with unary
functions, and so we have an implicit “the only parameter”.
For the sectioning operator, we used compose<> 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.
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., ),
a parameter name (e.g., my-irgb-rotate)
and identify the body. We should be ready to go.
color
(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.
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
, you'll see that
all it's doing is applying
irgb-greyscale-2 to a color,
and then applying irgb-average-component to the
result. That suggests that we can just use composition.
irgb-grey
(define irgb-greyscale-3 (compose irgb-grey irgb-average-component))
Checking whether this works as well is left as an exercise for the reader.
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
procedure
that computes the square of a number.
square
(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.
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 menu. Finally, click .
If things go wrong, see our instructions for running the CSC 151 software. Ask for help if you need it.
a. Verify that
correctly squares the
numbers 5, 10, -3, 1.2, and 0.05.
square
b. Check that
behaves as expected on the sample color values provided.
my-irgb-rotate
c. Check that the three versions of
behave as expected
on the sample color values provided.
irgb-greyscale-x
d. Using and
image-show, check that the three
versions of image-variant behave
as expected on the sample image provided.
irgb-greyscale-x
Write a procedure called
that takes a number and produce the multiplicative inverse of that
number (i.e., one divided by the number). For example:
invert
>(invert 2)1/2>(invert 1)1>(invert 2.0)0.5