Can anybody explain an example in Paul Graham's ANSI Common Lisp page 110?
The example try to explain the use &rest and lambda to create functional programming facilities. One of them is a function to compose functional arguments. I cannot find anything explaining how it worked. The code is as follows:
(defun compose (&rest fns)
(destructuring-bind (fn1 . rest) (reverse fns)
#'(lambda (&rest args)
(reduce #'(lambda (v f) (funcall f v))
rest
:initial-value (apply fn1 args)))))
The usage is:
(mapcar (compose #'list #'round #'sqrt)
'(4 9 16 25))
The output is:
((2) (3) (4) (5))
Line 2 and 6 look especially like magic to me. Any comments will be appreciated.
The
compose
function returns a closure that calls each of the functions from last to first, passing on the result of each function call to the next.The closure resulting from calling
(compose #'list #'round #'sqrt)
first calculates the square root of its argument, rounds the result to the nearest integer, then creates a list of the result. Calling the closure with say 3 as argument is equivalent to evaluating(list (round (sqrt 3)))
.The destructuring-bind evaluates the
(reverse fns)
expression to get the arguments ofcompose
in reverse order, and binds its first item of the resulting list to the fn1 local variable and the rest of the resulting list to the rest local variable. Hence fn1 holds the last item of fns,#'sqrt
.The reduce calls each the
fns
functions with the accumulated result. The:initial-value (apply fn1 args)
provides the initial value to thereduce
function and supports calling the closure with multiple arguments. Without the requirement of multiple arguments,compose
can be simplified to:destructuring-bind
combines destructors with binding. A destructor is a function that lets you access a part of a data structure.car
andcdr
are simple destructors to extract the head and tail of a list.getf
is a general destructor framework. Binding is most commonly performed bylet
. In this example,fns
is(#'list #'round #'sqrt)
(the arguments tocompose
), so(reverse fns)
is(#'sqrt #'round #'list)
. Thenis equivalent to
except that it doesn't bind
tmp
, of course. The idea ofdestructuring-bind
is that it's a pattern matching construct: its first argument is a pattern that the data must match, and symbols in the pattern are bound to the corresponding pieces of the data.So now
fn1
is#'sqrt
andrest
is(#'round #'list)
. Thecompose
function returns a function:(lambda (&rest args) ...)
. Now consider what happens when you apply that function to some argument such as4
. The lambda can be applied, yieldingThe
apply
function appliesfn1
to the argument; since this argument is not a list, this is just(#'sqrt 4)
which is2
. In other words, we haveNow the
reduce
function does its job, which is to apply#'(lambda (v f) (funcall f v))
successively to the#'round
and to#'list
, starting with2
. This is equivalent toOkay, here goes:
(#'sqrt #'round #'list)
), then sticks the first item intofn1
, and the rest intorest
. We have:fn1
=#'sqrt
, andrest
=(#'round #'list)
.(apply sqrt args)
(whereargs
are the values given to the resulting lambda) as the initial value, and with each iteration grabbing the next function fromrest
to call.(round (apply sqrt args))
, and the second iteration you end up with(list (round (apply sqrt args)))
.sqrt
in your case) is allowed to take multiple arguments. The rest of the functions are called with single arguments only, even if any particular function in the chain does a multiple-value return.