3.7 Basic Programming Examples (Hunk P)
3.7.7 Procedure Specialization, Composition, and Currying 1 Procedure Specialization
Suppose that we are writing a program where we need to take a list of numbers and produce a corresponding lists with numbers ten times as big.
Notice that we already have a procedure, map, that can iterate over a list, apply a function to
each item, and return the list of function values. We also have a multiplication procedure, *that
can multiply numbers by any value we want.
We can't just write (map * some-list), though, because whenmapiterates over a single list, it
expects a procedure that takes exactly one argument, and *takes two arguments. Somehow, we
need to supply the argument10 to each of the calls mapmakes to*.
What we need is a one-argument function that multiplies its argument by ten. We could dene our own multiplication-by-ten procedure, *10, and then use map to apply it to the elements of some-list.
(define (*10 number) (* 10 number)) (map *10 some-list)
Here we've specialized *to create*10|we've taken a function with some number of arguments,
and produced a function with fewer arguments, which is equivalent to calling the original procedure with the missing argument always the same.
If *10 is only used in one place, there's really no need to create a named procedure|we can
just use alambdaexpression to create the procedure where we need it, at the call to map: (map (lambda (number)
(* 10 number)) some-list)
Here we create an anonymous procedure that multiplies its argument by 10, and pass that procedure and a list tomap, which will map the procedure over the list and return the corresponding
list of results.
It is often a good idea to design procedures with specialization in mind.
Consider the similarities between member, memv, and memq. All of them do almost the same
thing, with the dierence being which equality test they use during a search.
We can dene a general procedure, mem, which expresses the similarities between these proce-
dures, and then specialize that procedure.
Our general procedure will look like member, except that it will take an argument saying which
test to use. In Scheme, this is easy|we can simply hand it a rst-class procedure like equal?or eq?, or any other test we want to use, and have it call that procedure to perform the test.
(define (mem test-proc thing lis) (if (pair? lis)
(if (test-proc (car lis) thing) lis
To get the eect of(member some-key some-list), we can write(mem equal? some-key some- list).
Note that here we're not calling equal?, we're just passing the value of the variable equal?
(i.e., the procedure rst-class procedure object equal?) to mem. mem receives this value when the
argument variable test-procis bound, and can call it by that name.
(In the *10 example, we specialized * with data|the number 10|but here we're specializing mem with a procedure. The same technique works, because procedures are data objects, and can
be passed as arguments like any other data, then called as procedures.)
If Scheme didn't providemember, we could easily dene it by specializingmem|we simply dene memberto call mem, but always pass equal?as the rst argument:
(define (member thing lis) (mem equal? thing lis))
Likewise, we could dene memqandmemvby specializing memwitheq?and eqv?, respectively.
This kind of function specialization is particularly useful when you have a pattern for a proce- dure, but may need arbitrary variants of it in the future.
For example, suppose you want to search a list of lists, and you want your search routine to return the rst sublist whose rst two elements match a particular two-element list. (This might be an ordered list of birthdays, and you could be searching for the part of the list that starts with a particular month of a particular year.)
You might search the list for any list whose rst elements are 1964and December, by handing
it a target list(1964 December), like this: (mem-first-two? '(1964 December) '((1961 January 15 "Susan") (1964 March 28 "Edward") (1964 March 29 "Selena") (1964 December 31 "Anton") (1965 January 8 "Booker"))))
((1964 December 31 "Anton") (1965 January 8 "Booker"))))
member, memq, and memv are useless for this, but it's pretty easy with mem. First we dene a
match predicate for our purpose:
(define (first-two-eqv? target thing) (and (eqv? (car target) (car thing))
(eqv? (cadr target) (cadr thing))))
Then we currymem with that predicate to create our search procedure: (define (mem-first-two? thing lis)
(mem first-two-eqv? thing lis))
Iffirst-two-eqv?is only likely to be used inmem-first-two, we can put it inside mem-first- two, as a local procedure, instead of leaving it hanging out where it can be called from other
procedures. This is a good idea for a procedure that is so specialized that it's unlikely to be useful in any other way|especially if you're sure it works for what you designed it for, but think it may be tricky to use for slightly dierent purposes. (For example, we've chosen to use theeqv?test for
matching list elements, but for some purposes this might be the wrong choice.)
(define (mem-first-two thing lis)
(let ((first-two-eqv? (lambda (target thing)
(and (eqv? (car target) (car thing))
(eqv? (cadr target) (cadr thing)))))) (mem first-two-eqv? thing lis)))
In this routine, first-two-eqv? is only called from one place|the call to mem. Rather than
dening it as a named procedure, usingletrecandlambda, we can simply use thelambdaexpres-
sion at the one place the procedure is needed:
(define (mem-first-two thing lis) (mem (lambda (target thing)
(and (eqv? (car target) (car thing)) (eqv? (cadr target) (cadr thing)))) target
This idiom is very common in situations where you need a small procedure in exactly one place. Likewise, if mem-first-two itself is only useful in one place, it would be reasonable to avoid
making it a procedure at all, and instead to simply call memfrom that place: ...
(mem (lambda (target thing)
(and (eqv? (car target) (car thing)) (eqv? (cadr target) (cadr thing)))) target
lis) ...