A bit of Lisp
July 18th, 2008 by mikeI’ve been reading this book on Lisp over at MIT press, a little at a time. Yesterday I read something that pushed me a little further along the path of understanding and I thought I’d share it here.
Many people who know functional programming say it’s indispensable, and others who aren’t familiar with it dismiss it as inefficient, academic, or irrelevant. I’d like to believe the first group, and am trying to work toward a real understanding . It is sometimes difficult to come from the perspective of a procedural programmer and see how it’s possible to achieve anything in a language that discourages the use of mutable variables. It takes a dramatic change in perspective — something I think I’m acquiring little by little.
Here are some excerpts from chapter 2 in the book. As they discuss a little in this section, it helps to think of functional programming in terms of data flow and circuits. I think really exploring this idea can lead to a lot of insight. Circuits can’t have mutable variables either, unless you introduce a clock. Using map you can “loop”, but I think it’s more consistent to think of it as parallel processing.
Here is an excerpt from section 2.2.3 of the book:
” the following procedure… takes a tree as argument and computes the sum of the squares of the leaves that are odd:
(define (sum-odd-squares tree)
(cond ((null? tree) 0)
((not (pair? tree))
(if (odd? tree) (square tree) 0))
(else (+ (sum-odd-squares (car tree))
(sum-odd-squares (cdr tree))))))On the surface, this procedure is very different from the following one, which constructs a list of all the even Fibonacci numbers Fib(k), where k is less than or equal to a given integer n:
(define (even-fibs n)
(define (next k)
(if (> k n)
nil
(let ((f (fib k)))
(if (even? f)
(cons f (next (+ k 1)))
(next (+ k 1))))))
(next 0))”
If you’re not familiar with lisp syntax and all that makes your eyes glaze over, those functions are very similar to how you would write the same algorithms in Java or C. The first uses recursion to traverse the tree, the second uses tail recursion analogous to an iterative loop to traverse the list, just like you would in C. The main thing to note is that almost half of the above code is taken up by control structures, since even the recursive calls are the equivalent of “i++” in a for loop.
After some discussion about a more abstract description of the computations and how to think in terms of signal processing, the book presents the same functions reformulated using the higher order functions enumerate, map, filter, and accumulate:
“Now we can reformulate sum-odd-squares and even-fibs as in the signal-flow diagrams. For sum-odd-squares, we enumerate the sequence of leaves of the tree, filter this to keep only the odd numbers in the sequence, square each element, and sum the results:
(define (sum-odd-squares tree)
(accumulate +
0
(map square
(filter odd?
(enumerate-tree tree)))))For even-fibs, we enumerate the integers from 0 to n, generate the Fibonacci number for each of these integers, filter the resulting sequence to keep only the even elements, and accumulate the results into a list:
(define (even-fibs n)
(accumulate cons
nil
(filter even?
(map fib
(enumerate-interval 0 n)))))”
Clearer? Hell yes… that is, once you internalize those extremely reusable higher order functions.
And the real gem that sums it all up, un-presuming and hidden away down in footnote #15:
“One of the reasons for the success of Lisp as a programming language is that lists provide a standard medium for expressing ordered collections so that they can be manipulated using higher-order operations.”
All this together led me to a bit of an “Ah ha!” moment, or rather, an “Ah ha!” half hour or so of deep thought.
I think the point I’m seeing is the enormous benefits of suppressing repetitive and verbose syntax of flow control. Instead you use flow control template functions like enumerate, map, filter and accumulate. This is only possible in a language where you can easily pass function pointers down for use by the flow control logic. The only way to do this in OOP is by using Polymorphism, which is a big reason why it’s one of The Pillars.
The only criticism I could think of against this new formulation of the functions was that by batching each step, you’re now required to allocate a new list after every step. However, another feature of most functional programming languages comes to the rescue: Lazy evaluation and call-by-need. Later in the program where the resulting lists would actually be used, the values will in fact be produced one at a time as they are requested from the list.
And this is a huge win in and of itself. Not the performance wins or parallelization available via lazy evaluation, but the fact that this kind of programming makes batch and on demand processing look exactly the same, all you’d need to do to switch between them would be to hint the run-time/compiler. Wow.