Fundamentals of Computer Science I: Media Computing (CS151.01 2008S)

Iterating Over Lists


Summary: We examine techniques for doing computation using each element of a list.

Introduction

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, (map func lst) creates a new list of the same length as 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.

Detour: Anonymous 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)

Using 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))

Reference

(foreach! func lst)
Traditional higher-order list procedure. Evaluate func on each element of the given list. Called primarily for side effects.
(map func lst)
Standard higher-order list procedure. Create a new list, each of whose elements is computed by applying func to the corresponding element of lst.

Creative Commons License

Samuel A. Rebelsky, rebelsky@grinnell.edu

Copyright (c) 2007-8 Janet Davis, Matthew Kluber, and Samuel A. Rebelsky. (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.

This work is licensed under a Creative Commons Attribution-NonCommercial 2.5 License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/2.5/ or send a letter to Creative Commons, 543 Howard Street, 5th Floor, San Francisco, California, 94105, USA.