I. Another way to get repetitive operations
Bryan Kennedy presented this topic in class for me, and
he posted his notes on one of our newsgroups. Here's my
brief (and not nearly as comprehensive) overview of the
material:
Recursion is just a nice way to get iterative ability out of
our computer programs without adding the complexity that
comes with the introduction of counter variables and
accumulator variables and assignment operations. How do we
know that things get more complex when we add that stuff?
Well, for starters, think about that nice simple substitution
model of evaluation---it's pretty understandable, no?
Imagine what that looks like if we add variables and
assignment. If you could keep track of it all, every
substitution involves keeping track of all the variables and
what they're bound to at every given step of the way...ugh.
It's no longer simple. In fact, the substitution model
becomes so cumbersome under these conditions that we have to
come up with other models of evaluation just to accommodate
the complexity. But I digress.
There's yet another way to get repetition out of our
procedures, and that falls under the header of "applicative
programming" as opposed to "recursive programming". To
demonstrate how applicative programming works, though, we'll
start with a recursive explanation of applicative
programming.
When using a programming language whose basic data structure
is the list, it's not surprising that one of the things we
want to do most often is perform the same procedure on all
the elements of a list. We already know how to do that
recursively. Say for example, that we want to take a list of
numbers as input and return a list of corresponding square
roots. That procedure might look like this (do you recognize
the recursion template?):
(define (lotsa-sqrts input-list)
(cond [(null? input-list) ()]
[else (cons (sqrt (car input-list))
(lotsa-sqrts (cdr input-list)))]))
and it would work like this:
> (lotsa-sqrts '(1 4 9 16 25))
(1 2 3 4 5)
>
Now, since we often want to do repetitive operations like
this, and most of the time that operation will probably be
something other than the square root procedure, wouldn't it
be nice if we could construct a generic function to which we
could pass another function and a list of things to do be
worked on in succession by that function? Well of course it
would! And folks, note here that when I say we want to pass
it a function, I don't mean passing it a function call to be
evaluated, as in (sqrt (* 2 2))...that's different. I mean
we want to pass the whole function definition as a parameter.
Eek! How the %$#@! are we gonna do that?!
II. The case of the headless function
In order to make this happen, there are some things you need
to know first. Remember way back when we were touting all
the things that made Scheme so nice? One of those features was
the lack of any procedure/data distinction. Here's one place
where that feature comes in handy.
Since a procedure can be treated as a piece of data, we can
decompose procedures into component parts. That means we can
separate a function body from its name. (How did that body
get that name? The "define" function did it, remember?) In
other words, it's possible (and in many cases desirable, as
we're about to see) to use a nameless, anonymous function
definition in our computations. Scheme of course has a
predefined function for separating function bodies from their
names, and it is named, ironically, "function". Here's how
it works:
Let's say you've defined a new function of your own, which in
this case takes a number as an argument and returns its
square. When you use "define" to create that function:
(define (sqr number)
(* number number))
you've told Scheme to bind the name "sqr" to everything that
follows the name "sqr" in your definition. That association
is then stored in the symbol table. Later, when you call the
"sqr" function, Scheme looks up "sqr" in the symbol table and
retrieves the function body associated with it, then uses
that function body to figure out what to do. If we want to
tell Scheme at some point to explicitly go to the symbol table,
look up the "sqr" function, retrieve the function body and
return it without doing anything else with it, then we can
just say:
> sqr
And in Dr. Scheme, we'll see this:
#
>
That's Dr. Scheme's way of saying "here's the procedure
that's associated with the name 'sqr'...you can't
recognize the function body, because it's compiled, but
here it is in case you want to use it in your computation."
If you could see the details somehow, it would be some
representation of the function body without the name,
which would look something like this:
(lambda (number)
(* number number))
It's a function body without its name; it's called a
lambda function.
III. What can you do with a lambda function?
Back to the original problem: how do we create a generic
function like "lotsa-sqrts" that isn't restricted to just
computing square roots? How do we get it to use any function
we pass it as an argument? How do we turn "lotsa-sqrts" into
"lotsa-anything"?
Let's borrow the "lotsa-sqrts" and make a few necessary
changes and see where that gets us:
(define (lotsa-anything function input-list)
(cond [(null? input-list) ()]
[else (cons (function (car input-list))
(lotsa-anything function (cdr input-list)))]))
We know we needed to pass a function body as an argument, so
we added that argument. Everything else is pretty much the
same, except we plugged in the parameter name "function" in
place of "sqrt". Will this work?
> (lotsa-anything sqrt '(1 4 9 16 25))
(1 2 3 4 5)
>
Of course it does! We pass the name of the function we want
to apply to all the elements of input-list to "lotsa-anything".
Scheme looks for the function body associated with that name
and does what we want. Most languages can't do this.
The net result is a function which maps another function onto
the elements of a list, performs that operation on each of
those elements, and returns a list of the results.
Because "lotsa-anything" sort of scoots along, mapping the
passed function body onto succeeding "car"s of the ever-shrinking
"input-list", this function resembles a function called "map".
The "map" function is already predefined in Scheme, and works
sort of like what we've defined here: it applies the given function
to successive "car"s of the list, collects the individual results
into a list, and returns that list. Despite the similarities, "map"
isn't the same as "lotsa-anything"---"map" is much more powerful.
So while building the "lotsa-anything" function gives us an idea
as to what the "map" function does, you should take the time to
look up "map" in the documentation that comes with Dr. Scheme and
become familiar with what's really going on there.
Here's how I can substitute the elegant, pre-defined "map" function
for my crude "lotsa-anything" slop and get the same result:
> (map sqrt '(1 4 9 16))
(1 2 3 4)
One thing that "map" can do that "lotsa-anything" can't is
take functions that expect more than one argument. If, for
example, I wanted to "cons" a bunch of elements of one list
into their respective elements of another list, I'd just do
this:
> (map cons '(a b c) '(1 2 3))
((a . 1) (b . 2) (c . 3))
>
(Dotted pairs...yuk! Anyway, we move on...)
Oh, I almost forgot to mention that whereever I'm passing the
name of a function, I could just pass a lambda body without a
name and things will work just fine:
> (map (lambda (number) (* number number)) '(1 2 3 4 5))
(1 4 9 16 25)
>
So "map" provides us with another means of getting repetitive
operations without the traditional looping structure.
Again, this style of programming is called "applicative
programming" to distinguish it from "recursion" or
"iteration".
Copyright (c) 2003 by Kurt Eiselt. All rights reserved, with
the exception of stuff that belongs to somebody else.
Last revised: December 8, 2003