Summary: We examine techniques for doing computation using each element of a list.
As you may recall from our initial discussions of algorithms, there
are four primary kinds of control that help us write algorithms: We
sequence operations, we choose between operations, we encapsulate
groups of operations into functions, and we repeat operations. At
this point, you've learned mechanisms for sequencing operations (either
by listing one after another or by nesting them), for choosing (either
cond or if), and for
grouping operations (as procedures).
However, you have not learned a general mechanism for repeating operations. It is time to remedy that situation. We will start by looking at how you iterate over the values in a list.
What if we want to iterate over other kinds of values? In a few days, you'll learn about recursion, Scheme's most general mechanism for repeating actions.
To ground these explorations, we will consider the ways in which these techniques might be useful for working with lists of spots, a representation of images we considered in previous readings.
map - Building New Lists from Old
Scheme provides one standard procedure for doing something with each
element of a list, map. In particular,
( creates a new list of the same
length as map func
lst)lst by applying the function
func to each
element of lst.
For example, we can add 1 to every number in a list of numbers with
(define increment (lambda (num) (+ 1 num))) (map increment numbers)
Let's see that code in a sample sequence from the interactions pane.
>(define numbers (list 11 2 1 8 16))>numbers(11 2 1 8 16)>(define increment (lambda (num) (+ 1 num)))>(map increment numbers)(12 3 2 9 17)>numbers(11 2 1 8 16)
As the last command suggests, map is pure: Even
after we've applied map, numbers
remains unchanged.
Note that map is a bit of a new kind
of procedure. Traditionally, our procedures have taken fairly
simple types as parameters (numbers, strings, image identifiers,
colors, lists, etc.). In contrast, map
takes a procedure as one of its parameters.
Using procedures as parameters (and even as return values) is a common
programming technique in Scheme and a few other languages. Procedures,
like map, that take other procedures as parameters
are called higher-order procedures.
Let's return to our first example of map.
(define numbers (list 11 2 1 8 16)) (define increment (lambda (num) (+ 1 num))) (map increment numbers)
What happens when the Scheme interpreter evaluates these two lines? Well, the definitions add the following pairs to the names table:
| Name | Value |
|---|---|
numbers |
(11 2 1 8 6) |
increment |
(lambda (num) (+ 1 num)) |
Next, the interpreter evaluates the call to map.
In order to evaluate the call to map, the
interpreter needs to evaluate all the parameters, which means that it
replaces the names in the call with their values.
(map (lambda (num) (+ num 1)) '(11 2 1 8 6))
Finally, it applies the procedure to each value. (How it does that is left as a mystery, at least for a little while longer.)
Now, you know that we could just as easily have used a list we created on-the-fly for the second parameter to map.
(map increment (list 1 2 3 4 5))
In this case, the Scheme interpreter needs to evaluate the second
parameter, rather than just look it up in the table. It can still look
up the value of increment though. After making
those substitutions, we end up wtih
(map (lambda (num) (+ num 1)) '(1 2 3 4 5))
By plugging in the list directly, we avoided the need for one definition.
That is, we no longer need to define numbers. Here's an
interesting thing: We can also manually write the lambda part of
increment. That is, we can start by writing
(map (lambda (num) (+ num 1)) (list 1 2 3 4 5))
No extra definitions are necessary!
A lambda-form without a corresponding define is
called an anonymous function (because it has
no name). Anonymous functions are regularly used along with procedures
like map to quickly string together actions.
For example, suppose we start with a list of numbers and, for each,
we want the maximum of 80 and that number. We can write
>(map (lambda (val) (max 80 val)) (list 22 88 23 66 100 90))(80 88 80 80 100 90)
map with Lists of Spots
The map procedure can be quite useful with lists
of spots. For example, if we've created one list of spots, we can map
a procedure onto that list to change the color of the spots, move the
spots elsewhere, or even rotate the spots around some point.
For example, in the lab on lists of spots, you wrote a procedure that translated a spot horizontally. We can translate a whole list of spots horizontally by 20 units with
(map (lambda (spot) (spot-htrans spot 20)) spots)
In effect, once you've designed a picture using lists of spots, you can use it as a kind of rubber stamp, drawing it again and again at different places in an image.
foreach! - Doing Something with Each Element of a List
Of course, for map to be useful with lists of spots,
we need a way to render all of the spots in a list, and not just one.
Of course, map provides a solution. We can draw all the
spots with something like the following:
>(map (lambda (spot) (image-render-spot! canvas spot)) spots)
This solution will certainly work. However, it's also a bit awkward. While we are doing something with each element in the list, our goal is not to create a new list. In this case, and many others, we iterate through the list not to create a list, but to do things with the values in the list, with no goal of creating new values.
This technique of iterating a list for the side effect, and not to
create a new list, is common enough that most Scheme programmers define
a procedure for just that purpose. That procedure is not in Standard
Scheme, but it's common enough that it has a common name,
foreach!.
So, here's the typical way to define a procedure that draws each spot in a list of spots.
;;; Procedure:
;;; image-render-spots!
;;; Parameters:
;;; image, an image
;;; spots, a list of spots
;;; Purpose:
;; Draw all of the spots on the image.
;;; Produces:
;;; [Nothing; Called for the side effect]
(define image-render-spots!
(lambda (image spots)
(foreach! (lambda (spot) (image-render-spot! image spot)) spots)))
We can write a similar procedure to draw a bigger version of each spot.
;;; Procedure:
;;; image-render-big-spots!
;;; Parameters:
;;; image, an image id
;;; spots, a list of spots
;;; Purpose:
;;; Render a list of spots "bigger".
;;; Produces:
;;; Nothing; called for the side effect.
;;; Preconditions:
;;; Each scaled spot can be safely rendered.
(define image-render-big-spots!
(lambda (image spots)
(foreach! (lambda (spot) (image-scaled-render-spot! image spot 20)) spots)))
We can even make the scale a parameter to the procedure.
;;; Procedure:
;;; image-scaled-render-spots!
;;; Parameters:
;;; image, an image
;;; spots, a list of spots.
;;; factor, a number
;;; Purpose:
;; Draw all of the spots in the list on the image, scaled by factor.
;;; Produces:
;;; [Nothing; Called for the side effect]
;;; Preconditions:
;;; factor >= 1
;;; The position of the scaled spot is within the bounds of the image.
;;; Postconditions:
;;; The image now contains a rendering of each spot.
(define image-scaled-render-spots!
(lambda (image spots scale)
(foreach! (lambda (spot) (image-scaled-render-spot! image spot scale))
spots)))
We're now ready to draw lots of copies of our sample image.
(image-render-spots! canvas spots) (image-render-spots! canvas (map (lambda (spot) (spot-htrans 2 spot)) spots)) (image-render-spots! canvas (map (lambda (spot) (spot-vtrans 3 spot)) spots)) (image-render-big-spots! canvas (map (lambda (spot) (spot-htrans 2 spot)) spots)) (image-render-big-spots! canvas (map (lambda (spot) (spot-vtrans 3 spot)) spots))
(foreach!
func
lst)
func on each element of the given list.
Called primarily for side effects.
(map
func
lst)
func to the corresponding element of
lst.