Fundamentals of Computer Science I: Media Computing (CS151.02 2007F)

Design Patterns and Higher-Order Procedures


Summary: In this reading, we explore the so-called higher-order procedures, procedures that take procedures as parameters or return procedures as results. In effect we are considering what happens when we allow procedures to serve as values.

Background: Design Patterns

One mark of successful programmers is that they identify and remember common techniques for solving problems. Such abstractions of common structures for solving problems are often called patterns or design patterns. You should already have begun to identify some patterns. For example, you know that procedures almost always have the form

(define procname
  (lambda (parameters)
    body))

You may also have a pattern in mind for the typical recursive procedure over lists:

(define procname
  (lambda (lst)
    (if (null? lst) 
        base-case
        (do-something (car lst) (procname (cdr lst))))))

In some languages, these patterns are simply guides to programmers as they design new solutions. In other languages, such as Scheme, you can often encapsulate a design pattern in a separate procedure.

A Simple Pattern: Apply a Procedure to Each Value in a List

Let's begin with a simple problem: Suppose we have a list of colors and we want to create three new lists: one that contains the red component of each color, one that contains the largest component of each color, and one that contains the average of the three components. Suppose also that your first inclination is to use recursion to create each list, perhaps because you've forgotten about map. After some reflection, you might write something like the following

;;; Procedure:
;;;   extract-red-components
;;; Parameters:
;;;   colors, a list
;;; Purpose:
;;;   Get the red component from each color in colors.
;;; Produces:
;;;   reds, a list
;;; Preconditions:
;;;   Every value in colors is an RGB color.
;;; Postconditions:
;;;   Every value in reds is an integer between 0 and 255, inclusive.
;;;   (length reds) = (length colors)
;;;   For each i, 0 <= i < (length colors)
;;;     (list-ref reds i) = (rgb.red (list-ref colors i))
(define extract-red-components
  (lambda (colors)
    (if (null? colors)
        ; If no colors remain, there's nothing left to work with,
        ; so use the empty list.
        null
        ; Otherwise, get the red component of the first color, get the
        ; red component of the remaining colors, and shove everything
        ; back together.
        (cons (rgb.red (car colors))
              (extract-red-components (cdr colors))))))

;;; Procedure:
;;;   extract-max-components
;;; Parameters:
;;;   colors, a list
;;; Purpose:
;;;   Get the maximum (largest) component from each color in colors.
;;; Produces:
;;;   maxs, a list
;;; Preconditions:
;;;   Every value in colors is an RGB color.
;;; Postconditions:
;;;   Every value in maxs is an integer between 0 and 255, inclusive.
;;;   (length maxs) = (length colors)
;;;   For each i, 0 <= i < (length colors)
;;;     (list-ref maxs i) = the maximum component of (list-ref colors i)
(define extract-max-components
  (lambda (colors)
    (if (null? colors)
        ; If no colors remain, there's nothing left to work with,
        ; so use the empty list.
        null
        ; Otherwise, get the maximum component of the first color, get 
        ; the maximum component of each remaining color, and shove 
        ; everything back together.
        (cons (max (rgb.red (car colors))
                   (rgb.green (car colors))
                   (rgb.blue (car colors)))
              (extract-max-components (cdr colors))))))

;;; Procedure:
;;;   extract-ave-components
;;; Parameters:
;;;   colors, a list
;;; Purpose:
;;;   Get the average component from each color in colors.
;;; Produces:
;;;   averages, a list
;;; Preconditions:
;;;   Every value in colors is an RGB color.
;;; Postconditions:
;;;   Every value in averages is an integer between 0 and 255, inclusive.
;;;   (length averages) = (length colors)
;;;   For each i, 0 <= i < (length colors)
;;;     (list-ref averages i) = the average component of (list-ref colors i)
(define extract-ave-components
  (let ((ave (lambda (c) 
               (round (/ (+ (rgb.red c) (rgb.green c) (rgb.blue c)) 3)))))
    (lambda (colors)
      (if (null? colors)
          ; If no colors remain, there's nothing left to work with,
          ; so use the empty list.
          null
          ; Otherwise, get the average component of the first color, get 
          ; the average component of each remaining color, and shove 
          ; everything back together.
          (cons (ave (car colors))
                (extract-ave-components (cdr colors)))))))

Here's how each works on the colors in the rainbow.

> (define rainbow (list color.red color.orange color.yellow color.green
                        color.blue color.indigo color.violet))
> (extract-red-components rainbow)
(255 255 255 0 0 102 79)
> (extract-max-components rainbow)
(255 255 255 255 255 255 79)
> (extract-ave-components rainbow)
(85 125 170 85 85 119 68)

What do these three procedures have in common? Most of the documentation, both the six P's and the internal documentation. All return null when given the null list. More importantly, all three do something to the car of the list, recurse on the cdr of the list, and then cons the two results together.

Hence, it is natural to design a general pattern for apply a procedure to every value in a list.

(define procname
  (lambda (lst)
    (if (null? lst)
        null
        (cons (modify (car lst)) 
              (procname (cdr lst))))))

We can even turn this pattern into a procedure.

;;; Procedure:
;;;   colors.transform
;;; Parameters:
;;;   colors, a list of colors.
;;; Purpose:
;;;   Applies TRANSFORM to each color in a list.
;;; Returns:
;;;   transformed, a list of colors.
;;; Preconditions:
;;;   lst contains only RGB colors. [Unverified]
;;;   TRANSFORM is defined, takes one color as a parameter
;;;     and returns a color. [Unverified]
;;; Postconditions:
;;;   transformed contains only RGB colors.
;;;   (length transformed) = (length colors)
;;;   For each i, 0 <= i < (length transformed)
;;;    (list-ref transformed i) = (TRANSFORM (list-ref colors i))
(define colors.transform
  (lambda (colors)
    ; If no elements remain, we have nothing to apply TRANSFORM to,
    ; so stick with the empty list.
    (if (null? colors) null
        ; Otherwise, transform the first value, transform the remaining
        ; values, and put the stuff back together into a list.
        (cons (TRANSFORM (car colors))
              (colors.transform (cdr colors))))))

Where does TRANSFORM come from? We could define it first (as the preconditions suggest). For example, here's an application of colors.transform in which we get just the red component of a color.

> (define TRANSFORM rgb.red)
> (colors.transform rainbow)
(255 255 255 0 0 102 79)

Similarly, we can get the average component by defining TRANSFORM as we defined the local ave from extract-ave-components.

> (define TRANSFORM
    (lambda (c) (round (/ (+ (rgb.red c) (rgb.green c) (rgb.blue c)) 3))))
> (colors.transform rainbow)
(85 125 170 85 85 119 68)

You may find this strategy of defining TRANSFORM first inelegant. We certainly do. It also won't work for all cases. For example, what if we want to get the red components of all the colors in one list and then get the average components of all the colors in another list? We'd have to redefine TRANSFORM right in the middle of the code. Ugly.

Is there a better solution? Yes! Instead of forcing TRANSFORM to be defined before we call colors.transform, we can make the procedure a parameter to the pattern. For example, here's a new version of colors.transform that takes the extra parameter.

;;; Procedure:
;;;   colors.transform
;;; Parameters:
;;;   colors, a list of colors.
;;;   trans, a procedure 
;;; Purpose:
;;;   Applies trans to each color in a list.
;;; Returns:
;;;   transformed, a list of colors.
;;; Preconditions:
;;;   lst contains only RGB colors. [Unverified]
;;;   trans takes one color as a parameter and returns a color. [Unverified]
;;; Postconditions:
;;;   transformed contains only RGB colors.
;;;   (length transformed) = (length colors)
;;;   For each i, 0 <= i < (length transformed)
;;;    (list-ref transformed i) = (trans (list-ref colors i))
(define colors.transform
  (lambda (colors trans)
    ; If no elements remain, we have nothing to apply transform to,
    ; so stick with the empty list.
    (if (null? colors) null
        ; Otherwise, transform the first value, transform the remaining
        ; values, and put the stuff back together into a list.
        (cons (trans (car colors))
              (colors.transform (cdr colors) trans)))))

If we plug in procedures (either previously-named procedures or newly named procedures), we get the results we expect.

> (colors.transform rainbow rgb.red)
(255 255 255 0 0 102 79)
> (define rgb.max (lambda (c) (max (rgb.red c) (rgb.green c) (rgb.blue c))))
> (colors.transform rainbow rgb.max)
(255 255 255 255 255 255 79)
> (colors.transform rainbow rgb.green) 
(0 119 255 255 0 0 47)

You should have observed a very important (and somewhat stunning) moral from this example, procedures can be parameters to other procedures. We call the procedures that take other procedures as parameters higher-order procedures.

Of course, the idea of passing a procedure as a parameter should be comfortable if you've been using map a lot. In fact, map is likely to be defined much like colors.transform (except that officially, the order in which map builds the result list is up to the implementer). We could even define our own version.

;;; Procedure:
;;;   proc.map
;;; Parameters:
;;;   proc, a procedure 
;;;   lst, a list 
;;; Purpose:
;;;   Applies proc to each value in a list.
;;; Returns:
;;;   newlst, a list 
;;; Preconditions:
;;;   proc can successfully be applied to each value in lst.
;;; Postconditions:
;;;   newlst is the same length as lst
;;;   For each i, 0 <= i < (length newlst)
;;;    (list-ref newlst i) = (proc (list-ref lst i))
(define proc.map
  (lambda (proc lst)
    ; If no elements remain, we can't apply anything else,
    ; so produce the empty list.
    (if (null? lst) 
        null
        ; Otherwise, apply the procedure to the first value, 
        ; apply the procedure to the remaining values, and 
        ; put the results back together into a list.
        (cons (proc (car lst)) 
              (proc.map proc (cdr lst))))))

We can now return to the starting problems. What if we want to get the red component of every color in a list? We map rgb.red onto the list.

> (proc.map rgb.red rainbow)
(255 255 255 0 0 102 79)

What if we want the average component? We might use let to name the ave helper, and then use map with that.

> (let ((ave (lambda (c) 
               (round (/ (+ (rgb.red c) (rgb.green c) (rgb.blue c)) 3)))))
    (proc.map ave rainbow))
(85 125 170 85 85 119 68)

Since we can write expressions like that, we would no longer need to write extract-ave-components. We probably wouldn't even need to write extract-red-components or extract-max-components.

That observation suggests a second important moral: Once you've defined a few higher-order procedures, like map, you can often avoid writing other procedures, since the higher-order procedures let you write more general expressions.

For example, if we have a list of colors, and we want to complement each color in the list, we can use colors.transform, proc.map, or even map.

> (define complementary-rainbow (proc.map rgb.complement rainbow))

Higher Order Predicates: Are they All Whatever?

There are many advantages to encoding design patterns in higher-order procedures. An important one is that it stops us from tediously writing the same thing over and over and over again. Think about writing the predicates all-integer?, all-rgb?, all-spot?, and so on and so forth. We've done so many times. However, as our colleague, John Stone, says,

Writing and testing one of these definitions is an interesting and instructive exercise for the beginning Scheme programmer. Writing and testing another one is good practice. Writing and testing the third one is, frankly, a little tedious. If we then move on to [others], eventually programming is reduced to typing.

So, how do we avoid the repetitious typing? We begin with one of the procedures.

;;; Procedure:
;;;   all-int?
;;; Parameters:
;;;   lst, a list
;;; Purpose:
;;;   Determine if all of the values in lst are integers.
;;; Produces:
;;;   ok?, a Boolean
;;; Preconditions:
;;;   [Standard]
;;; Postconditions:
;;;   If there is an i such that (integer? (list-ref lst i))
;;;     fails to hold, then ok? is false.
;;;   Otherwise, ok? is true.
(define all-int?
  (lambda (lst)
    (or (null? lst)
        (and (integer? (car lst))
             (all-int? (cdr lst))))))

Next, we identify the parts of the procedure that depend on our current type (i.e., that everything is a list).

(define all-WHATEVER?
  (lambda (val)
    (or (null? val)
        (and (WHATEVER? (car val))
             (all-WHATEVER? (cdr val))))))

Finally, we remove the dependent part or parts from the procedure name and make them parameters to the modified procedure.

;;; Procedure:
;;;   all?
;;; Parameters:
;;;   pred?, a unary predicate
;;;   lst, a list
;;; Purpose:
;;;   Determine if pred? holds for all the values in lst.
;;; Produces:
;;;   ok?, a Boolean
;;; Preconditions:
;;;   [Standard]
;;; Postconditions:
;;;   If there is an i such that (pred? (list-ref lst i))
;;;     fails to hold, then ok? is false.
;;;   Otherwise, ok? is true.
(define all?
  (lambda (pred? lst)
    (or (null? lst)
        (and (pred? (car lst))
             (all pred? (cdr lst))))))

Here's how we might test whether something is a list of numbers.

> (all integer? (list 1 2 3))
#t
> (all integer? (list 1 "two" 3))
#f

We can also define all-int? using the all procedure.

(define all-int?
  (lambda (lst)
    (all integer? lst)))

The results are the same.

> (all-int? (list 1 2 3))
#t
> (all-int? (list 1 "two" 3))
#f

Built-in Higher-Order Procedures

We have seen that it is possible to write our own higher-order procedures. Scheme also includes a number of built-in higher-order procedures. You can read about many of them in section 6.4 of the Scheme report (r5rs), which is available through the DrScheme Help Desk. Here are some of the more popular ones.

You already know about the basic use of the map procedure. After our discussion above, you also know how one might implement it. It turns out that map has some additional capabilities. For example, you can apply a procedure to multiple lists (in which case it takes the corresponding value from each list).

One of the most important built-in higher-order procedures is apply, which takes a procedure and a list as arguments and invokes the procedure, giving it the elements of the list as its arguments:

> (apply string=? (list "foo" "foo"))
#t
> (apply * (list 3 4 5 6))
360
> (apply append (list (list 'a 'b 'c) (list 'd) (list 'e 'f)
                       null (list 'g 'h 'i)))
(a b c d e f g h i)

Anonymous Procedures, Revisited

Recall that in the examples above we wrote something like

> (let ((ave (lambda (c) 
               (round (/ (+ (rgb.red c) (rgb.green c) (rgb.blue c)) 3)))))
    (map ave rainbow))

Let's take this code apart. The let expression makes ave the name of a procedure of one parameter that takes a color and computes the average components. The body of the let then applies this newly-defined ave to each element of rainbow.

As you may recall from our discussion of Scheme's evaluation strategy, when Scheme sees the name of a procedure, it substitutes the body of the procedure. Hence, to Scheme, the code above is essentially equivalent to

(map (lambda (c) (round (/ (+ (rgb.red c) (rgb.green c) (rgb.blue c)) 3))) 
     rainbow)

Because Scheme does this substitution, we can also do it. That is, we can write the previous code and have Scheme execute it.

> (map (lambda (c) (round (/ (+ (rgb.red c) (rgb.green c) (rgb.blue c)) 3))) 
       rainbow)
(85 125 170 85 85 119 68)

So, what does this new code say? It says “Apply a procedure that averages the components to every element of the list rainbow.” Do we care what that procedure is named? No. We describe procedures without names as anonymous procedures. In effect, you can read lambda as “a procedure”. Hence, (lambda (params) body) can be considered a shorthand for “a procedure of parameters params that computes body”.

You may recall using anonymous procedures along with higher-order procedures such as map and image.map!. You will find that you frequently use anonymous procedures with design patterns and higher-order procedures.

Returning Procedures

Just as it is possible to use procedures as parameters to procedures, it is also possible to return new procedures from procedures. For example, here is a procedure that takes one parameter, a number, and creates a procedure that multiplies its parameters by that number.

;;; Procedure:
;;;   make-multiplier
;;; Parameters:
;;;   n, a number
;;; Purpose:
;;;   Creates a new procedure which multiplies its parameter by n.
;;; Produces:
;;;   proc, a procedure of one parameter
;;; Preconditions:
;;;   n must be a number
;;; Postconditions:
;;;   (proc v) = n * v
(define make-multiplier
  (lambda (n) ; n is the parameter to make-multiplier
    ; Return value: A procedure
    (lambda (v) (* n v))))

Let's test it out.

> (make-multiplier 5)
#<procedure>
> (define timesfive (make-multiplier 5))
> (timesfive 4)
20
> (timesfive 101)
505
> (map (make-multiplier 3) (list 1 2 3))
(3 6 9)

We can use the same technique to build the legendary compose operation that given two functions, f and g, builds a function that applies g and then f.

;;; Procedure:
;;;   compose
;;; Parameters:
;;;   f, a unary function
;;;   g, a unary function
;;; Purpose:
;;;   Functionally compose f and g.
;;; Produces:
;;;   fun, a unary function.
;;; Preconditions:
;;;   f can be applied to any values g generates.
;;; Postconditions:
;;;   fun can be applied to any values g can be applied to.
;;;   fun generates values of the type that f generates.
;;;   (fun x) = (f (g x))
(define compose
  (lambda (f g) ; f and g are the parameters to compose
    ; compose returns a procedure of one parameter
    (lambda (x)
      ; that procedure applies g, and then applies f.
      (f (g x)))))

Here are some fun tests.

> (define add2 (lambda (x) (+ 2 x)))
> (define mul5 (lambda (x) (* 5 x)))
> (define fun1 (compose add2 mul5))
> (fun1 5)
27
> (fun1 3)
17
> (define fun2 (compose mul5 add2))
> (fun2 5)
35
> (fun2 3)
25

You may also recall the left-section and right-section (or l-s and r-s) procedures. We can define these as higher-order procedures as well.

Especially when using map, we often write anonymous procedures that look something like the following.

  (lambda (num) (* 2 num))

We can write a general pattern as follows:

  (lambda (val) (BINARY-PROC ARG1 val))

Wouldn't it be nice if we could avoid repeating ourselves so much? left-section helps us do that.

(left-section binary-proc arg1) creates a new procedure by filling in the first argument of a binary procedure. For example, we might define a procedure that multiplies a value by 2 with

> (define mul2 (left-section * 2))

The code for left-section is relatively straightforward.

(define left-section
  (lambda (binproc arg1)
    ; Build a new procedure of one argument
    (lambda (arg2)
      ; That calls binproc on the appropriate arguments
      (binproc arg1 arg2))))

(right-section binary-proc arg2) creates a new procedure by filling in the second argument of a binary procedure. For example, we might define a procedure that subtracts 3 from a value with

> (define sub3 (right-section - 3))

How is right-section defined? We leave that as an exercise for the reader.

Creative Commons License

Samuel A. Rebelsky, rebelsky@grinnell.edu

Copyright 2007 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.