Cluster 5 * Summer 2004
Computer Science Through Games and Scheme Programming
David G. Kay, Dan Frost, Kevin Papke
Scheme Lab Activities: Fourth Set
-
We talked in class about multiple-valued data:
Homogeneous (all-one-type), which we build in Scheme with lists, and heterogeneous
(a collection of possibly different types of data), which we build in Scheme
with structures. You've seen these both in the restaurant program,
but now let's look in more detail at how they work.
-
In the restaurant program, we use a structure
to store the information about a restaurant. (Those familiar with object-oriented
programming can think of a structure as a light-weight class.) We define
a structure--we give the blueprint for building restaurant objects--with
define-struct
(you can find this line in the restaurants program):
(define-struct
Rrant (name cuisine phone dish price))
This says, "To build a restaurant object,
you need five fields (five compartments for holding five different
data items). We'll use the name Rrant to refer to the category or class
of restaurant objects; we'll use the words name, cuisine, phone, dish,
and price to refer to the five fields."
The structure definition above doesn't
say what the five fields represent, but we need to be clear about it: In
order, they're the name of the restaurant (a string), the restaurant's
cuisine (the kind of food they serve, a string), their phone number (a string,
since it might have parentheses and a hyphen in it), the name of the best
dish they serve (a string), and the price of that dish (a number).
-
When we enter that definition, Scheme automatically
creates for us:
-
A constructor, make-Rrant,
for building new restaurant objects:
(make-Rrant "McDonalds" "burgers"
"343-4344" "French Fries" "1.95")
-
Accessor/selector functions for each of the
five fields:
(define R1 (make-Rrant "Pascal" "French"
"343-4343" "Escargots" "9.95"))
(Rrant-name R1),
which returns Pascal
(Rrant-cuisine R1),
which returns French,
and so on.
-
And a couple of other functions that we don't
need to worry about now.
-
Because structures can take so many different
forms, there's no automatic way that would effectively print out every
structure. So it's useful for us to write a function that prints out
our structure's contents clearly:
(define Rrant-print
(lambda (r)
(display "Name: ") (display
(Rrant-name r)) (newline)
(display "Cuisine: ") (display
(Rrant-cuisine r)) (newline)
(display "Phone: ") (display
(Rrant-phone r)) (newline)
(display "Best dish: ") (display
(Rrant-dish r)) (newline)
(display "Price: ") (display
(Rrant-price r)) (newline)))
To save some space above, we have combined
two displays
and a newline
on each line.
-
Write a definition for the function Rrant-cheap?
that takes a restaurant (i.e., an Rrant
structure) as its input and returns true if the restaurant's (best dish's)
price is less than $10.00 and false otherwise. Test your function well
(which should involve defining a few restaurants and using them as arguments
to Rrant-cheap?).
-
The $10.00 figure above is an arbitrary constant.
We'd have a more flexible function if we could specify the threshold
as an input (argument, parameter) to the function. So write the function
Rrant-cheap2?
that takes two arguments, a restaurant and a number, and returns true if
the restaurant's price is less than the specified number (and false
otherwise).
-
Write a function called Rrant-increase-price
that takes a restaurant and a number as its input and returns a restaurant
that's identical to the input restaurant except that its price is increased
by the number. (Do this functionally, by creating a new restaurant structure
with the field values from the old structure, except for the changed price.)
-
Write a function called Rrant-change-price
that takes a restaurant and a number and returns a restaurant with its price
field increased or decreased by the number (positive or negative). Before
you start coding, think about the functions you have already defined. Can
you do this simply by changing a name?
-
Write a function called Rrant-same-cuisine?
that takes two restaurants and returns true if their cuisine fields are
the same (use the function equal?
to compare the strings) and false otherwise.
-
Now let's look at lists.
-
Scheme gives us these two constructors for
lists:
-
list,
which takes its arguments and combines them into a list
(list 3 4 5)
returns (3 4 5)
-
cons, which takes an item and a list and creates
a new list by inserting the item at the front of the original list
(cons 3 (list 5 7))
returns (3 5 7)
(cons 3 empty) returns
(3)
(cons (list 1 2) (list 3 4)) returns
((1 2) 3 4)
Take a minute to make sure you get that one.
-
Scheme gives us these two accessors for lists:
-
first,
which takes a list and returns its first item
-
rest,
which takes a list and returns the list with its first item removed (but
note that it doesn't actually change its input; it just returns a new,
different copy)
-
Scheme gives us these two other useful functions
for lists:
-
empty?,
which takes a list and returns true if it's empty and false otherwise
(also called null?)
-
length,
which takes a list and returns the number of elements in the list.
-
One way of describing a list is like this:
A list is either:
-- empty,
or
-- (cons
item
list)
So empty
is a list, because it's right there as one part of our definition.
("Hi")
is a list because it's the same as (cons
"Hi" empty); this follows
from the second line of our definition because we already know empty
is a list. ("Oh"
"Hi") is a list because it's
the same as (cons
"Oh" (cons "Hi" empty)),
and we already know (cons
"Hi" empty) is a list. And
so on. This pattern helps us write functions that process lists.
-
How do we write a function that processes
all the items on a list? Suppose we want to write a function called double-a-list
that takes a list of numbers as its input and returns a list containing
each number from the original list, doubled.
We already know how to take a single number
and double it:
(define double
(lambda (n)
(* 2 n)))
We can use the pattern above to apply this
function to each item on the list, in turn:
; double-a-list: list of numbers -> list of
numbers
; Take a list of numbers and return a list with
each number doubled
(define double-a-list
(lambda (L)
(cond
((empty? L) empty)
(else (cons (double (first L)) (double-a-list
(rest L)))))))
; Examples
(double-a-list empty) "Should return"
empty
(double-a-list (list 3 4 5)) "should return"
(list 6 8 10)
Let's take this a line or two at a time.
-
The first two lines (starting with semicolons)
are comments--English annotations to the human reader that don't have
any effect on the computer's actions.
-
We call the first line the "contract":
It gives the name of the function, the type of data each argument will
be, and the type of value that it will return.
-
The second line describes the purpose or semantics
of the function.
-
The define,
lambda, and cond
lines should be familiar.
-
The two cond clauses match the two lines of
our pattern.
-
The ((empty?
L) empty) line says that if the list
we're looking at is empty, there's nothing to double--the result
we should return (if we're trying to double all the elements on an empty
list) is just the empty list itself.
-
The else
line says that if the list isn't empty, then we know it's made up
of an item and a list, just as the pattern says. What we need to do for
our new list is double the first item (double
(first L)), and then do this process
all over again on the rest of the list (double-a-list
(rest L)).
Does it bug you that we call the function
double-a-list
from within its own definition? Doesn't it seem as if that kind of
circular definition could get us in trouble? It could, but in this case
it doesn't, because the definition isn't truly circular. It's
more of a spiral, because each time we repeatedly call double-a-list,
it's with the rest
of the current list--that is, we keep calling with smaller and smaller lists,
and eventually we'll call it with empty.
When that happens, we'll do the first line of the cond
and we'll stop repeating.
This process is called recursion, and it's
an extremely powerful idea that we'll see again and again.
-
The two example lines have been cleverly constructed
so that if you run them, they serve as tests of the function's operation.
Go ahead; copy the whole definition into DrScheme and click Execute.
-
Write the function exponentiate
that takes two arguments, a list of numbers and a single number, and returns
a list with the original numbers all raised to the power of the single number.
(exponentiate (list)) "should return"
empty
(exponentiate (list 4 3 2 1) 2) "should return"
(16 9 4 1)
(exponentiate (list -2 0 3) 3) "should return"
(-8 0 27)
-
Suppose we have a list of restaurant objects
as defined above. For processing purposes, that's no different than
a list of numbers. The structure of the list is the same; it's just
each item that's a little bigger. If we know how to process an item,
we know how to process a list of them.
Write the function change-prices
that takes a list of restaurant objects as its input and returns a list
of the same restaurants, but with each restaurant's price increased
by $2.00.
COSMOS Cluster 5 Home
* COSMOS UCI Home
David G. Kay, 406B Computer
Science
University of California, Irvine
Irvine, CA 92697-3425
-- (949) 824-5072
-- Fax (949) 824-4056
-- Email
kay@uci.edu
Tuesday, July 13, 2004 -- 1:08 PM