Summary: We explore why and how you might define your own procedures in Scheme.
As you may recall from a recent reading, we designed a simple drawing data type that we can use to describe simple images. We can also render those drawings on GIMP images. Here are a few simple shapes.
(define black-circle (hshift-drawing 30 (vshift-drawing 40 (scale-drawing 50 drawing-unit-circle))))
(define purple-ellipse (vshift-drawing 10 (hshift-drawing 30 (vscale-drawing 25 (hscale-drawing 50 (recolor-drawing "purple" drawing-unit-circle))))))
(define blue-i (recolor-drawing "blue" (drawing-group (hshift-drawing 4 (vshift-drawing 4 (scale-drawing 8 drawing-unit-circle))) (hshift-drawing 4 (vshift-drawing 28 (hscale-drawing 8 (drawing-vscale drawing-unit-square 32)))))))
(define red-eye (hshift-drawing 32 (vshift-drawing 12 (drawing-group (hscale-drawing 64 (vscale-drawing 24 (recolor-drawing "darkgrey" drawing-unit-circle))) (hscale-drawing 60 (vscale-drawing 20 (recolor-drawing "white" drawing-unit-circle))) (hscale-drawing 16 (vscale-drawing 20 (recolor-drawing "red" drawing-unit-circle)))))))
Here's what they look like when rendered.
black-circle
purple-ellipse
blue-i
red-eye
In these examples, as well as in some previous readings and labs, you have seen that many algorithms require more than just one or two procedure calls. For example, to build a black circle of radius 25, centered at 40, 30, we need to
We've just seen code to do just that.
(define black-circle (hshift-drawing 30 (vshift-drawing 40 (scale-drawing 50 drawing-unit-circle))))
More generally, given a radius, an x coordinate, and a y coordinate, we might write
(define radius 25) (define x-center 45) (define y-center 35) (define my-circle (hshift-drawingx-center
(vshift-drawingy-center
(scale-drawing (* 2radius
) drawing-unit-circle))))
Clearly, that's a bit cumbersome to write every time we want a circle. What we'd really like to do is to say is that “this group of code is a procedure that takes as input a radius, the x coordinate of the center, and the y coordinate of the center”.
You know that Scheme has procedures, since
you've used both built-in procedures -- such as
,
and sqrt
-- and MediaScheme extensions, such as
*
and
hshift-drawing
. But can you
define your own procedures? Certainly. How and why
you do so are the primary topics of this reading.
drawing->image
Before we progress to our own
procedure (which we've just seen takes three parameters), let's consider
a simpler procedure, one that takes just one parameter. In particular,
we will consider a procedure that makes a variant of a drawing that is
scaled up by 25%, shifted right 20 pixels, and then down another 15.
(Why would we want such a variant? Here's one reason: If we recolor
the variant, it could serve as a “shadow” for the original drawing.
However, for now, we just use it as an interesting alternative to the first
drawing.)
circle
For any of our drawings above, the code is straightforward. For example,
(define black-circle-1 (hshift-drawing 15 (vshift-drawing 20 (scale-drawing 1.25 black-circle)))) (define purple-ellipse-1 (hshift-drawing 15 (vshift-drawing 20 (drawing-scale 1.25 purple-ellipse))))
black-circle-1
purple-ellipse-1
In fact, we might even combine an image with its variant.
(drawing-group black-circle black-circle-1)
(drawing-group purple-ellipse purple-ellipse-1)
However, we've written very similar code to define
black-circle-1
and purple-ellipse-1
. We
would certainly benefit from combining the code into a single procedure.
So, how do you define 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.
For the two procedures mentioned above, we might choose the names
(build a variant of
a drawing) and variant-1
(pair a drawing with its variant). In each case, the procedure will
take one parameter, a drawing.
variant-pair-1
In essence, to define the body of the first
procedure, all we need to do is to call our parameter
and use the code from
above, with drawing
in place of
drawing
black-circle
or purple-oval
.
(define variant-1 (lambda (drawing) (hshift-drawing 15 (vshift-drawing 20 (scale-drawing 1.1 drawing)))))
This brand new procedure can now be called as it were a built-in procedure. For example,
(variant-1 blue-i)
(variant-1 red-eye)
Pairing an image with its variant is even easier. We just build an image group of the image and its variant.
(define variant-pair-1 (lambda (drawing) (drawing-group drawing (variant-1 drawing))))
(variant-pair-1 blue-i)
(variant-pair-1 red-eye)
One potential deficiency of
(and, therefore, with variant-1
)
is that it always uses the same horizontal and vertical offset.
Arguably, we might want to make the offset depend on the size of the
original drawing. Fortunately, we can obtain some information on
drawings. In particular,
pair-variant-1
(
gives the width of a drawing and
drawing-width
drawing
)(
gives its height.
drawing-height
drawing
)
To make a right neighbor to a drawing, we could simply make a copy of the drawing that is is offset horizontally the width of the first drawing.
(define add-right-neighbor (lambda (drawing) (drawing-group drawing (hshift-drawing (drawing-width drawing) drawing))))
(add-right-neighbor purple-ellipse)
(add-right-neighbor blue-i)
While the procedure above is intended to build a drawing, 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 more such procedures when we examing Scheme's numeric values in more depth.
Let's return to the earlier example: How do we build a circle of a particular radius, centered at a particular point? Let's look at the “general” code we wrote earlier.
(define my-circle (hshift-drawingx-center
(vshift-drawingy-center
(scale-drawing (* 2radius
) drawing-unit-circle))))
What do we need to change to turn this definition of a single
circle into a procedure? Not much. We just choose a name
(e.g.,
), add a
circle
lambda
to make it a function, choose the
parameters (
,
radius
, and
x-center
), and add the generalized
code from above.
y-center
(define circle (lambda (radius x-center y-center) (hshift-drawing x-center (vshift-drawing y-center (scale-drawing (* 2 radius) drawing-unit-circle )))))
(circle 10 5 5)
(circle 50 100 120)
What would we do if we wanted to color our circles? We could add another
parameter to the procedure to represent the color. What would we do to draw a rectangle, rather
than a circle? We'd probably replace
with both
radius
and
width
. You will have the
opportunity to try developing procedures like these in the lab.
height
Copyright © 2007-2014 Janet Davis, Matthew Kluber, Samuel A. Rebelsky, and Jerod Weinman. (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 3.0 Unported License
.