Sketchy LISP |
Copyright (C) 2006,2007
Nils M Holm Buy a copy at Lulu.com |
An Introduction to Functional Programming in Scheme
| Previous Section | - Contents - Index - | Next Section |
A higher order function is a procedure that either takes a procedure as an argument or returns a procedure (or both). For instance, the complement and compose procedures introduced earlier in this book were higher order functions (HOFs). A classic among the HOFs is the mapcar procedure, which used to be part of early versions of LISP. Mapcar maps a procedure over a list. It is defined as follows:
(define (mapcar a f)
(cond ((null? a) '())
(else (cons (f (car a))
(mapcar (cdr a) f)))))
Using this procedure you can do quite a few useful things like computing the sum of squares of a set of numbers:
(define (sum-of-squares . x) (apply + (mapcar x (lambda (x) (* x x)))))
or find the member with the least absolute value in a list of numbers:
(define (least-abs x) (apply min (mapcar x abs)))
In both of these procedures, mapcar creates an argument
list for a variadic procedure such as + or min.
This combination can be seen frequently in Scheme programs. The following
sample reduction of sum-of-squares illustrates how
apply and mapcar interact:
(sum-of-squares 2 3 5 7) -> (apply + (mapcar (2 3 5 7) (lambda (x) (* x x)))) -> (apply + (4 9 25 49)) -> (+ 4 9 25 49) => 87
The Scheme standard does not define mapcar but a much
more powerful procedure called
map.
You may have observed
that mapcar can only apply procedures of one argument to the
members of a single list. Map eliminates this limitation.
It can be applied to any number of lists. To allow for this, the
procedure argument had to move to the first position. Map
can do anything mapcar can do and more, like computing
the inner product (the dot product
) of two vectors:
(define (v* a b) (apply + (map * a b)))
or the sum of two vectors:
(define (v+ a b) (map + a b))
or even the sum of any number of vectors:
(define (v+ . a) (apply map + a))
In the above defintion,
apply takes
three arguments rather than two. In fact apply itself
is variadic. When more than two arguments are passed to it, the
second through second-to-last arguments are consed to the last
argument:
(apply map + a) -> (map + a)
(apply list 2 3 4 '(5 6 7)) -> (list 2 3 4 5 6 7)
Map is a variadic procedure which accepts
a number of lists, so the structure it ultimately processes
is a list of lists. Like mapcar it passes the car parts
of these lists to its procedure argument. It then advances to the
cdr parts of its list arguments.
The version of map that will be constructed here
will stop as soon as it reaches the end of one of the lists passed
to it. Subsequently it processes lists only up to the length
of its shortest argument:
(map + '(1 2 3) '(4 5)) => (5 7)
Note that this behavior, although found in some environments,
does not comply with the Scheme standard, which states that all
lists passed to map must have the same length.
A procedure for testing whether one member of a list is equal to
() can easily be created by first introducing another
useful predicate: any? tests whether a given list
a contains any member satisfying a predicate p:
(define (any? p a)
(cond ((null? a) #f)
((p (car a)) #t)
(else (any? p (cdr a)))))
Using this predicate, the any-null? procedure is trivial:
(define (any-null? x) (any? null? x))
Procedures for taking the car parts and the cdr parts of the sublists of a list can be defined by means of mapcar:
(define (car-of a) (mapcar a car)) (define (cdr-of a) (mapcar a cdr))
Given these procedures, the definition of map is quite
straight-forward. The procedure basically looks like mapcar,
but uses car-of instead of car, cdr-of
instead of cdr and any-null? instead of
null?. And because a real map has to run
in constant space, the following version is tail-recursive, too:
(define (map f . a)
(letrec
((map2
(lambda (a b)
(cond ((any-null? a) (reverse b))
(else (map2 (cdr-of a)
(cons (apply f (car-of a)) b)))))))
(map2 a '())))
Note that this procedure would accept zero lists, which the real
map procedure does not. However, as mentioned earlier,
standard Scheme provides no procedure for indicating failure, so
this condition is left unchecked. (You may use bottom
or an implementation dependent error reporting facility to improve
this version of map.)
One question that comes into mind is this: If map can
do anything mapcar can do, can the occurrences of
mapcar in car-of and cdr-of
be replaced with map? Can mapcar be optimized
away completely by doing this? The startling answer is: no
.
(Before you go ahead: can you figure out on your own why this
optimization is not possible?)
The answer is given in a semi-formal way by assuming that car-of is written in this way:
(define (cdr-of a) (map cdr a))
Given this definition, a sample application of map
is reduced:
(map - '(1 2 3)) -> (map2 ((1 2 3)) ()) -> (cond ((any-null? ((1 2 3))) (reverse b)) ...) -> (cond (else (map2 (cdr-of ((1 2 3))) ...)))
The ellipses in the above reduction denote that this part of the expression is currently of no interest. What is interesting at this point is that
(map cdr ((1 2 3)))
has to be evaluated in order to get the value of first argument of map2. It is evaluated like this:
(map cdr ((1 2 3))) -> (map2 ((1 2 3)) ()) -> (cond ((any-null? ((1 2 3))) (reverse b)) ...) -> (cond (else (map2 (cdr-of ((1 2 3))) ...)))
At this point (map cdr ((1 2 3)))
has to be evaluated again in order to continue the reduction.
In other words: To compute the first argument of map2,
the first argument of map2 has to be computed.
This
is a recursive statement without a trivial case, so its reduction can
never terminate.
No reduction containing the same intermediate step twice (like above
example) can ever reduce to a normal form, because such a repetition
indicates a cycle in that reduction. If you can show that
the evaluation of an expression contains a cycle, you have proven
that the expression in question has no normal form. In terms of
programming: you have proven that the program will crash
when it is run. Do you think that this can be done in imperative
languages?
In an earlier section, two versions of the depth procedure were shown. The section ended with the question whether depth can be written in any simpler way. Using higher order procedures, indeed, it can:
(define (depth a)
(if (pair? a)
(+ 1 (apply max (map depth a)))
0))
Higher order functions are a quite powerful tool. They actually
do add expressiveness to a language, because they allow it to do
things that could not be done without them. Another previous section
explained why if
cannot be implemented as an ordinary
procedure. The reasoning was as follows. Given a procedure xif
which implements if, the expression
(letrec
((down
(lambda (n)
(xif (zero? n)
0
(down (- n 1))))))
(down 1))
would reduce to bottom, because both the trivial and the recursive branch of
(xif (zero? n) 0 (down (- n 1)))
would be evaluated before xif was applied
and therefore the procedure could never terminate. Higher order
functions help here, too, because they can be used to wrap up
expressions for later evaluation. Consider the expression
(lambda () (down (- n 1)))
This expression reduces to a procedure whose body is the branch that causes trouble in the above application of xif. Because the troublesome expression is wrapped up in a procedure, it is not evaluated before the procedure is applied (to zero arguments) by writing:
((lambda () (down (- n 1))))
(Note the additional parentheses!) The following equation holds for any expression x:
x = ((lambda () x))
The following implementation of xif is based on this equality:
(define (xif pred true false) (if (pred) (true) (false)))
It accepts three zero-argument procedures which wrap up the predicate, the true branch and the false branch of the conditional expression, so
(if a b c)
has the same meaning as
(xif (lambda () a) (lambda () b) (lambda () c))
Therefore above expression involving the application of down can be re-written using xif:
(letrec
((down
(lambda (n)
(xif (lambda () (zero? n))
(lambda () 0)
(lambda () (down (- n 1)))))))
(down 1))
Except for its awkward syntax xif is perfectly equal
to if. The above expression even reduces in constant
space, so any value can be passed to down safely.
In fact, higher order functions are so powerful that
lambda
alone is sufficient to create an entire programming
language. Doing so is beyond the scope of this book, but here is
a little teaser: In the section explaining primitive procedures,
these were described as procedures that cannot be implemented in
Scheme itself easily. Here is how the basic list processing
primitives can be re-written in Scheme using only lambda
and variables:
(define (kons a b) (lambda (p) (p a b))) (define (kar x) (x (lambda (a b) a))) (define (kdr x) (x (lambda (a b) b)))
Let us see if this works:
(kons 'head 'tail) => #<procedure (p)>
Does not look too promising, does it? But wait. A cons cell is
an object that glues together two other objects, where the first one
can be extracted using car and the second one can be
extracted using cdr, and this is exactly what
kar and kdr do with objects created using
kons:
(kar (kons 'head 'tail)) => head (kdr (kons 'head 'tail)) => tail
Here is an explanation how it works. Note that the external representations of procedures include bodies here and the variables of procedures are substituted by their values to make the code easier to read.
(kons 'head 'tail)
=> #<procedure (p) (p head tail)>
(kdr (kons 'head 'tail))
-> (kdr #<procedure (p) (p head tail)>)
-> (#<procedure (x) (x (lambda (a b) b))>
#<procedure (p) (p head tail)>)
-> (#<procedure (p) (p head tail)> (lambda (a b) b))
-> ((lambda (a b) b) head tail)
=> tail
Even recursion can be implemented using lambda
exclusively by using
self-application:
(define (S f x) (f f x))
Using S, the recursive fact procedure from the first section can be re-written in this way:
(S (lambda (f x)
(cond ((zero? x) 1)
(else (* x (f f (- x 1))))))
5)
=> 120
This expression uses an anonymous recursive procedure to compute the factorial of 5, but how can a procedure apply itself when it has no name?
A procedure that is passed to S has to provide an
additional argument which will be bound to a copy of itself. This is
what the f argument of the above lambda function is used for.
(There is a more general device known as the Y combinator
that does not have this limitation. It will be introduced later.)
S applies the procedure f to two arguments: f itself and 5. Here is another depiction of the anonymous factorial procedure. This time, S is expanded to a lambda function and the first argument of the operator is rendered in boldface characters:
((lambda (f x) (f f x))
(lambda (f x)
(cond ((zero? x) 1)
(else (* x (f f (- x 1))))))
5)
You might want to try to evaluate this expression on a piece of paper in order to understand what it does. You may need a lot of paper, though, which is why there is no sample reduction in this book.
| Previous Section | - Contents - Index - | Next Section |