Procedures are values in Scheme. A definition such as
(define square
(lambda (number)
(* number number)))
associates the name square with the procedure that is the
value of the lambda-expression, just as a definition such as
(define side 125)
associates the name side with the number that is the value of
the numeral 125. When the Scheme processor subsequently
evaluates the expression
(square side)
it determines the meaning of square in exactly the same way as
it determines the meaning of side. Procedures, in short, are
managed like any other values.
Since procedures are values, Scheme permits you to supply them as arguments to other procedures, so that they can be bound to parameters of those other procedures. So we programmers can abstract general patterns of computation from the particular procedures that are used in particular computations, just as we abstract patterns of computation from the particular lists or numbers that we operate on in particular computations.
For example, here are the definitions for three procedures that share a pattern. Each takes one argument. The first determines whether its argument is a list of numbers; the second, whether its argument is a list of strings; the third, whether its argument is an association list, a list of pairs.
(define list-of-numbers?
(lambda (whatever)
(or (null? whatever)
(and (pair? whatever)
(number? (car whatever))
(list-of-numbers? (cdr whatever))))))
(define list-of-strings?
(lambda (whatever)
(or (null? whatever)
(and (pair? whatever)
(string? (car whatever))
(list-of-strings? (cdr whatever))))))
(define association-list?
(lambda (whatever)
(or (null? whatever)
(and (pair? whatever)
(pair? (car whatever))
(association-list? (cdr whatever))))))
Writing and testing any one of these definitions is an interesting and
instructive exercise for the beginning Scheme programmer. Writing and
testing the second one is good practice. Writing and testing the third one
is, frankly, a little tedious. If we then move on to
list-of-symbols?, list-of-integers?,
list-of-ballots?, and so on, eventually programming is reduced
to typing.
We need to automate the process, now that it's become purely routine. We
should write a single procedure that takes, as its argument, the predicate
that we want the elements of a list to satisfy and returns, as its value,
the appropriate list-testing predicate. Let's call this delightful,
time-saving procedure list-of. If it were a primitive of
Scheme, we could simply write
(define list-of-numbers? (list-of number?)) (define list-of-strings? (list-of string?)) (define association-list? (list-of pair?)) (define list-of-symbols? (list-of symbol?))
and so on, and the results would be just as if we had actually typed out the full definitions of those procedures.
The procedure list-of is not a primitive of Scheme, but we can
define it ourselves. Here's a step-by-step method for figuring out what
the definition of list-of looks like.
We first make up a template showing the common structure of the bodies of
the definitions of procedures that we're abstracting from, with a generic
identifier, predicate, in place of the part that varies. We
also use a generic identifier, recurrer, to name the template
itself, when we need to invoke it recursively. Here is the template:
(lambda (whatever)
(or (null? whatever)
(and (pair? whatever)
(predicate (car whatever))
(recurrer (cdr whatever)))))
We embed this template inside a letrec-expression that binds
it to the identifier recurrer and then, in its body, simply
repeats that name, so that the template procedure itself is the value of
the whole expression:
(letrec ((recurrer (lambda (whatever)
(or (null? whatever)
(and (pair? whatever)
(predicate (car whatever))
(recurrer (cdr whatever)))))))
recurrer)
When we invoke the list-of procedure, we give it a predicate
as its argument, and list-of drops this predicate into the
correct position in the template, completing the construction of the
specialized version of recurrer, which it then returns.
Here's how it looks in Scheme:
(define list-of
(lambda (predicate)
(letrec ((recurrer (lambda (whatever)
(or (null? whatever)
(and (pair? whatever)
(predicate (car whatever))
(recurrer (cdr whatever)))))))
recurrer)))
Here are three procedures with similar structures. The first one finds out how many even numbers there are in a given list of integers; the second finds out how many whitespace characters there in a given list of characters; the third one finds out how many of the elements of a given list are symbols.
(define even-tally
(lambda (ls)
(cond ((null? ls) 0)
((even? (car ls)) (+ (even-tally (cdr ls)) 1))
(else (even-tally (cdr ls))))))
(define whitespace-tally
(lambda (ls)
(cond ((null? ls) 0)
((char-whitespace? (car ls)) (+ (whitespace-tally (cdr ls)) 1))
(else (whitespace-tally (cdr ls))))))
(define symbol-tally
(lambda (ls)
(cond ((null? ls) 0)
((symbol? (car ls)) (+ (symbol-tally (cdr ls)) 1))
(else (symbol-tally (cdr ls))))))
By abstracting what these procedures have in common, define and test a
procedure tallier that takes a predicate as its argument and
constructs and returns a specialized tallying procedure that counts the
number of elements of a given list that satisfy the predicate.
Once we have a ``higher-order'' procedure like list-of or
tallier to work with, we can even construct our own predicates
and pass them as arguments. For example, suppose that we want a procedure
that checks whether its argument is a list of exact multiples of seven. We
could define this in two stages, thus:
(define exact-multiple-of-seven?
(lambda (whatever)
(and (integer? whatever)
(zero? (remainder whatever 7)))))
(define list-of-exact-multiples-of-seven?
(list-of exact-multiple-of-seven?))
Alternatively, we could write the lambda-expression that
denotes the predicate right into the call to list-of:
(define list-of-exact-multiples-of-seven?
(list-of (lambda (whatever)
(and (integer? whatever)
(zero? (remainder whatever 7))))))
How would you use the tallier procedure that you defined in
exercise 1 to create procedures to determine
'n/a (``not
available'') in a list? (Hint: Use a lambda-expression.)Here are three procedures, each of which takes a natural number as argument and returns a list:
(define generate-list-of-squares
(lambda (len)
(let kernel ((remaining len)
(so-far null))
(if (zero? remaining)
so-far
(let ((next (- remaining 1)))
(kernel next (cons (* next next) so-far)))))))
(define generate-list-of-hyphen-strings
(lambda (len)
(let kernel ((remaining len)
(so-far null))
(if (zero? remaining)
so-far
(let ((next (- remaining 1)))
(kernel next (cons (make-string next #\-) so-far)))))))
(define generate-list-of-termials
(lambda (len)
(let kernel ((remaining len)
(so-far null))
(if (zero? remaining)
so-far
(let ((next (- remaining 1)))
(kernel next (cons (termial next) so-far)))))))
(The termial procedure was defined in the lab on recursion with natural
numbers; it finds the sum of all the natural numbers up to and
including its argument.)
Apply each of these procedures to a few small natural numbers to see what they do.
Define and test a generate-list procedure that abstracts the
common structure of these three definitions. Generate-list
should take, as its argument, any one-argument procedure proc
that can be applied to a natural number. Generate-list should
return, as its value, a procedure that, when applied to a natural number
len, constructs and returns a list of the results of applying
proc to every natural number less than len.
Scheme provides several built-in procedures that take procedures as
arguments. One is map, which takes as arguments a procedure
and a list and applies the procedure to each element of the list,
collecting the results into a list:
> (map square (list 3 -9 8/7 0)) (9 81 64/49 0)
The map procedure works as if it were defined like this:
(define map
(lambda (procedure ls)
(if (null? ls)
null
(cons (procedure (car ls)) (map procedure (cdr ls))))))
Here's how map could be used to write the lengths
procedure from the lab on recursion
with lists.
(define lengths
(lambda (ls)
(map length ls)))
Use map to give a similarly concise definition of a procedure
association-list-keys that takes one argument, an association list, and returns a list of
the keys for that association list.
The map procedure can actually take more than two arguments,
if all of the extras are lists:
> (map string-append (list "left" "start" "beginning")
(list "-to-" "-to-" "-to-")
(list "right" "finish" "end"))
("left-to-right" "start-to-finish" "beginning-to-end")
> (map cons (list 'a 'b 'c) (list 'd 'e 'f))
((a . d) (b . e) (c . f))
Exercise 3.2 on page 81 of the textbook asks you to define a procedure
pairwise-sum that takes as arguments two lists of numbers,
equal in length, and returns a new list whose components are the sums of
the corresponding components of the arguments. Define this procedure using
map.
Another useful ``higher-order'' procedure that is built into Scheme 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)
Exercise 3.3 on page 82 of the textbook asks you to define a procedure
dot-product that takes as arguments two lists of numbers,
equal in length, and returns the sum of the products of corresponding
elements of the arguments:
> (dot-product (list 1 2 4 8) (list 11 5 7 3)) 73 ; ... because (1 x 11) + (2 x 5) + (4 x 7) + (8 x 3) = 11 + 10 + 28 + 24 = 73 > (dot-product null null) 0 ; ... because in this case there are no products to add
Use apply and map to give an extremely concise
definition of this procedure.
This document is available on the World Wide Web as
http://www.cs.grinnell.edu/~stone/courses/scheme/procedures-as-values.xhtml
created March 24, 1997
last revised March 17, 2000