Defining Your Own Scheme Procedures


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

Drawings, Revisited

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

A Problem: Repeated Computations

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

  • Scale the unit circle by 50.
  • Horizontally shift that circle 40 units to the right.
  • Vertically shift that circle 30 units down.

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-drawing
   x-center
   (vshift-drawing
    y-center
    (scale-drawing 
     (* 2 radius)
     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 sqrt, and * -- and MediaScheme extensions, such as hshift-drawing and drawing->image. But can you define your own procedures? Certainly. How and why you do so are the primary topics of this reading.

A Simpler Problem: Varying Drawings

Before we progress to our own circle 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.)

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.

Defining Procedures

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.

Procedure to Vary Images

For the two procedures mentioned above, we might choose the names variant-1 (build a variant of a drawing) and variant-pair-1 (pair a drawing with its variant). In each case, the procedure will take one parameter, a drawing.

In essence, to define the body of the first procedure, all we need to do is to call our parameter drawing and use the code from above, with drawing in place of 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)

Adding Neighbors

One potential deficiency of variant-1 (and, therefore, with pair-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, (drawing-width drawing) gives the width of a drawing and (drawing-height drawing) gives its height.

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)

Computing with Non-Images

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 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 more such procedures when we examing Scheme's numeric values in more depth.

Making Shapes

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-drawing
   x-center
   (vshift-drawing
    y-center
    (scale-drawing 
     (* 2 radius)
     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., circle), add a lambda to make it a function, choose the parameters (radius, x-center, and y-center), and add the generalized code from above.

(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 radius with both width and height. You will have the opportunity to try developing procedures like these in the lab.


Jerod Weinman

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.

Creative Commons License This work is licensed under a Creative Commons Attribution-NonCommercial 3.0 Unported License .