Skip to main content

Defining your own procedures

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

Introduction

We’ve been studying Scheme for only a few days. And, those few days, you’ve already found yourself using a wide variety of procedures, from simple arithmetical operations like + to more complex list operations like map and reduce. We’ve even seen a way to create “new” procedures by combining existing procedures using the procedure composition operations, o. For example, (o increment increment) gives you a procedure that adds two to its parameter.

As you continue to work with Scheme, you will find that it is useful to define and name your own procedures. Why? Most frequently, because having names for a sequence of operations can clarify the meanings of those operations. Which of the following would you rather read?

> (average my-ratings)

or

> (/ (reduce + my-ratings) (length my-ratings))

Fortunately, Scheme provides a variety of ways to create and name procedures. In this reading, we will explore a few the core ones.

Naming procedures

Not so surprisingly, we most frequently name procedures in the same way we name any kind of value, using define. Just as we can write (define answer 42) to allow us to use the name answer in place of the value 42, we can write (define plus +) and then use plus as an operation.

> (define plus +)
> (plus 1 5 2)
8
> (plus 8 3)
11
> (plus 1)
1
> (plus)
0

However, we want to do more than associate names with existing procedures; we want to define our own sequence of operations. There are three core techniques: We can compose existing one-parameter procedures, we can fill in some parameters of an existing procedure, and we can use a general procedure form known as lambda. We cover each in turn.

Procedure composition

You’ve already seen the basics of procedure composition. (o f g) creates a procedure that applies g to its parameter and then applies f to the result.

> (define increment-then-square (o square increment))
> (define square-then-increment (o increment square))
> (increment-then-square 5)
36
> (square-then-increment 5)
26
> (increment-then-square 8)
81
> (square-then-increment 8)
65
> (define add3 (o increment increment increment))
> (add3 5)
8
> (add3 2+4i)
5+4i
> (add3 "hello")
Error! . . increment: contract violation
Error!  expected: number?
Error!  given: "hello"
Error!  in: the 1st argument of
Error!      (-> number? number?)
Error!  contract from: <pkgs>/csc151/numbers.rkt
Error!  blaming: anonymous-module
Error!   (assuming the contract is correct)
Error!  at: <pkgs>/csc151/numbers.rkt:13.5

Procedure sectioning

As you might guess, there are some things we cannot easily do with composition. For example, suppose we want to write a procedure, half, that takes a number as input and divides that input by two. In this case, we don’t have anything to build upon, other than division, and division is traditionally a binary procedure. We want to write something like

> (define half (/ ? 2))

where the ? is intended to represent the parameter to half. However, we can’t write that, because as soon as DrRacket sees (/ ...) it says to itself “I should divide that first thing by the second” and we don’t want it to do the division immediately. We just want to say “When someone calls half on a value, evaluate (/ that-value 2).”

What do we do? The csc151/hop library provide a procedure known as section that lets you fill in some of the arguments to a procedure. Instead of writing (/ ? 2), we write (section / <> 2). As you’ve probably guessed, the <> is supposed to be the “here’s the input to our function”; we think it’s supposed to look like an empty space. And, instead of putting the division sign immediately after the open paren, we write the word section. The section delays the evaluation until later.

Let’s try it.

> (define half (section / <> 2))
> (half 10)
5
> (half 7)
3 1/2
> (half 8.4)
4.2
> (half 4+5i)
2+5/2i
> (half 0+6i)
0+3i

That looks pretty good, doesn’t it? Note, however, that the placement of the <> is important. Since (/ a b) computes a divided by b, and we want to divide by 2, the <> comes immediately after the /. We call that the “left section” of a binary procedure.

What happens if we make the <> the second parameter of /? (We call that the “right section”.) Let’s see.

> (define flah (section / 2 <>))
> (flah 10)
1/5
> (flah 7)
2/7
> (flah 0+6i)
0-1/3i
> (flah 0)
Error! . . ../../Applications/Racket v6.5/collects/racket/private/kw.rkt:929:25: /: division by zero

As these examples suggest, flah divides 2 by whatever number you give it.

We can also use multiple <>’s in a section when we have a procedure that takes more than two parameters.

> (define this-and-that (section list <> "and" <>))
> (this-and-that "ham" "eggs")
'("ham" "and" "eggs")
> (this-and-that "self gov" "the individually advised curriculum")
'("self gov" "and" "the individually advised curriculum")
> (this-and-that "Lyles" "Bobs")
'("Lyles" "and" "Bobs")

Lambda expressions

It seems like there’s an awful lot we can do with composition and sectioning. And there is. But there are still some things that are not possible. For example, you may recall that we computed the average of a list as follows:

  • Sum the number of elements in the list with (reduce + ...).
  • Compute the number of elements in the list with length.
  • Divide the sum by the length.

Neither o nor section has a mechanism for doing two independent computations and then combining them. While such a mechanism may exist, at this point we’re going to switch to the most general form of procedure definition, the “lambda expression”. The term “lambda” is Scheme’s word for “procedure”. (We’ll explain why sometime. It’s Sam’s great great grand advisor’s fault.) When we want to say “a procedure that takes inputs a and b and computes exp”, we write

(lambda (a b)
  exp)

For example, in defining a procedure, average, the input is numbers and the expression is the Scheme code for “divide the sum by the length”.

(define average
  (lambda (numbers)
    (/ (reduce + numbers)
       (length numbers))))

Let’s see if it works.

> (average (list 5 1 4))
3 1/3
> (average (iota 10))
4 1/2
> (average (list 1 3))
2

More generally, we define procedures with lambda as follows.

(define PROCEDURE
  (lambda (PARAM-1 ... PARAM-N)
    EXP-1
    EXP-2
    ...
    EXP-n))

When you call such a procedure, it substitutes all of the arguments for the parameters in the expressions and then evaluates them one by one. At the end, it gives you the value of the last expression.

As you progress through the course, you will find yourself defining procedures in many ways lambda expressions will be the most common.

Benefits of abstraction

At the beginning of this reading, we suggested that one of the key benefits of being able to define your own procedures is clarity. That is, it is often much easier to read an expression that uses a procedure you’ve named than it is to read the underlying code that is used to implement the procedure.

The use of a name for a section of code is one of the ways we use the concept of “abstraction” in programming. That is, in saying what we are doing without explaining how, we are abstracting away some of the details.

But there are benefits to abstraction beyond readability. We might, for example, discover a more efficient way to compute a value. If we have the same expression for computing that value at a variety of places in our program, we will have to spend a good deal of time updating all of those places. And when we make that many updates, something is likely to break. But if we’ve named that computation (that is, created a procedure or subroutine), then we only have to update the code in one place.

As we think about working with sets of data, we may also find that our policies for “cleaning” or otherwise manipulating the data change. For example, the first time through a group of data sets, we might decide to round numbers to the nearest multiple of ten. However, later, after examining the data more closely, we may discover that some of the data were collected only in multiples of 100. In thinking about how to address that problem, we might then decide to round the remaining data to nearest multiples of 100. If we just use (clean data) to clean our data, and defined the clean process separately, we only have to update one part of our program.

Self checks

Check 1: Subtracting two

Give three ways to define a procedure, subtract2, that takes a number as input and subtracts 2 from that number.

  • Using o . Note that decrement, which subtracts one from its parameter, can be found in the library csc151/numbers.
  • Using section.
  • Using a lambda expression.

Check 2: Bounding values

You may recall that we used the following expression to bound a value between lower and upper.

(min (max val lower) upper)

Write a procedure, bound-grade, that takes a real number as input and bounds it between 0 and 100.