Defining your own procedures
- We explore why and how you might define your own procedures in Scheme.
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
reduce. We’ve even seen a way to create
“new” procedures by combining existing procedures using the procedure
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)
> (/ (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.
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
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.
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
As you might guess, there are some things we cannot easily do with
composition. For example, suppose we want to write a procedure,
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))
? is intended to represent the parameter to
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
half on a value, evaluate
(/ that-value 2).”
What do we do? The
csc151/hop library provide a procedure
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
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
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
<> 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
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")
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
- Divide the sum by the length.
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
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.
Check 1: Subtracting two
Give three ways to define a procedure,
subtract2, that takes a number
as input and subtracts 2 from that number.
o. Note that
decrement, which subtracts one from its parameter, can be found in the library
- Using a lambda expression.
Check 2: Bounding values
You may recall that we used the following expression to bound
a value between
(min (max val lower) upper)
Write a procedure,
bound-grade, that takes a real number as input
and bounds it between 0 and 100.