My love affair with Lisp
I don’t always agree with Paul Graham, but his writings are often thought-provoking. One of the things he’s done recently is to rekindle my interest in Lisp and Scheme.
I think I originally ran across Lisp by reading Gödel, Escher, Bach, then by downloading the Xlisp package on a PC clone. I started seriously using it in Intro to AI class in college. (That was also my introduction to object-oriented programming, which at the time was the big up-and-coming buzzword. It turned me off of OO for a time, since I thought it extremely silly to have to create a window, then ask it to display itself; I couldn’t see how this was an improvement over simply “draw a window”). After that, I pretty much switched to C (and later Perl) and Lisp was just something I used to customize Emacs.
Then something I read of Graham’s rekindled my interest in Lisp. As I said at the time, it was like meeting that girl you knew and didn’t like in High School, and finding that she’s actually smart and fun and sexy in an odd sort of way.
One thing Graham discusses is “the Blub problem”. Let’s say that all of the programming languages are ordered by “power”. Square in the middle is a language we’ll call Blub. A long-time Blub programmer looks down at less powerful languages and thinks, “How can those guys write code in a language that doesn’t even have <feature>?”, where <feature> is some feature that Blub has, but that less-powerful languages don’t. Then the Blub programmer looks up at a language more powerful than Blub, which has some feature that Blub doesn’t have, and thinks “Who needs that? I can do everything I want in Blub.”
Closures
Lisp has closures, and they’re an integral part of Scheme. The term “closure” has a precise technical meaning that I don’t remember at the moment, but in this context, it means a chunk of code, along with its accompanying context, accessible as a first-class object. That is, you can define a function as
(set! my-function (lambda (n) (+ 1 n)))
then pass my-function around like an ordinary variable, or call it like a function. Or, if you have
(let ((k 10)) (set! my-function (lambda (n) (+ k n))))
not only will the value of my-function be a chunk of code that you can call like a function, but that chunk of code will “remember” that k existed, and that it was set to 10, when it was created.
C and C++ have pointers to functions; C++ has operator(), which allows you to call an object as if it were a function. And, of course, you can do all sorts of fancy tricks to carry the relevant context with you as necessary. But Lisp does this for you automatically.
Note, too, that this kinda takes the wind out of the STL’s sails. A clever application of the template mechanism in C++ allows you to write fairly generic code: you can write a sort algorithm that doesn’t explicitly mention a comparison function; or code that applies a given transformation to every element in an array, without saying what that transformation is when you write the code. Meanwhile, Lisp has had (map function list) since forever.
Heck, in C/C++, if you want to use a pointer-to-function, you first have to define a (named) function for the pointer to point to. Lambda expressions in Lisp mean that you can have a chunk of code without bothering to give it a name (unless you want to).
Continuations
I just discovered continuations yesterday. (Actually, I think I rediscovered them. I looked them up in one of my Lisp textbooks, and one of the section headings was annotated. I suspect that, like objects, I didn’t appreciate at the time what it was that I was looking at.)
I’m probably misunderstanding a lot and omitting a lot of details, but here’s how I understand it: at any point during a program’s execution, there’s “what has already been done” and “the rest of the program”. A continuation is “the rest of the program”, along with accompanying context. Lisp’s call/cc function allows you to place a bookmark in the current flow of execution, give that bookmark a name, then jump back to it (by calling it like a function. Duh).
More specifically, when you call a continuation foo with a value, say 15, you’re saying “remember that bookmark foo? Well, to make a long story short, “the rest of the code” at that point evaluates to 15. Now jump back there and continue.” This allows you to do short-circuit evaluation (the canonical example is that of multiplying a list of numbers. If you ever run across a zero, you can just say “long story short, the product is zero”, and not waste time looking at the rest of the list). try/catch/throw frequently aren’t language primitives, but are implemented in terms of continuations.
Macros
Back when I was learning C and the most advanced language I knew was Pascal, I thought the C preprocessor was pretty hot stuff. And, of course, C++ has things like templates, which give you type-checking as well. But Lisp macros blow those away.
Simply put, a Lisp macro is a chunk of Lisp code that’s evaluated at compile-time instead of run-time. Among other things, it can evaluate to another Lisp expression, which will be fed to the compiler. This allows you to define new variables and functions at compile-time. Yes, C macros can do that as well, but Lisp macros can contain arbitrary Lisp code.
Let’s say that your code is going to be doing a lot of trigonometry, and for efficiency, you want to precompute the value of sin(x), cos(x), and tan(x) from zero to two pi, in increments of 0.1. In C/C++, as far as I know you have to either call an initialization function at run-time, or else write a separate program or script to generate a header file with double sines[] { ... }, then arrange in your Makefile to have this header file rebuilt as necessary, and so forth. In Lisp, you can just generate the arrays at compile-time (if you’re willing to take the time hit, of course).
Or let’s say that you’re a C++ programmer who’s thoroughly convinced that it’s a Good Thing for members to be private, and to use accessors everywhere. Now, in many cases, the accessors will be trivial and identical:
class MyClass { private: int x; public: int get_x(void) { return x; } void set_x(int value) { x = value; } };
If you add another member, the code will be identical except for the name and possibly the type. This is just begging for some syntactic sugar. You could, of course, write a GENERIC_MEMBER(type,name) preprocessor macro, and have your class declaration contain a bunch of GENERIC_MEMBER() calls.
Okay, but since templates can generate classes on demand, and the STL allows you to iterate over any kind of data structure, it seems you should be able to write something like
class MyClass { SpecialType special_member; WeirdType weird_member; public: int something(void); double do_nifty_thing(int x, string y); AND_GENERIC_MEMBERS( int x, int y, string z, ); };
but as far as I can tell, you can’t. But I’ve done this in Scheme.
Lisp macros are also heavily used as syntactic sugar, to effectively change the syntax of the language (at least, to the extent that Lisp has syntax). That is, you can group related declarations (say, a command-line option’s name, default value, and descriptive text for --help‘s output) together.
I suspect that experienced Lisp hackers snigger at the idea of a tool like yacc/bison. Why bother with a separate tool and its attendant Makefile headaches, they think, when it’s simpler to just write a series of lines like (def-syntax assignment lvalue "=" rvalue)?
The Down Sides
Having said all this, there are down sides to Lisp. For one thing, few people know it. I write Perl and shell scripts at work, because they’ll have to be maintained by my coworkers when I leave or go on vacation. Writing Lisp code would be like putting up signs in Latin. Sure, it may be compact and stuff, but I’d get stuck maintaining it.
As a side effect, it’s not necessarily that easy finding a library that does what you want. In C or Perl, you can find libraries for everything from DNS lookups to drawing widgets to regressive analysis, in myriad flavors to suit all tastes. There are fewer Lisp hackers, so there are fewer canned packages. And if you don’t feel like being a Morlock today and want to install a Debian package or a BSD port, your choices are even more limited. Plus, given the number of Lisp dialects out there, a module written for a different dialect may not work out of the box in your environment (heck, just look at the number of Emacs modules that say things like “if I’m running under XEmacs, do this thing; if I’m running under GNU Emacs, do this other thing”). I never thought I’d list “community” as a language feature, but there you go.
There’s also the fact that Lisp’s clean syntax, like Tcl’s, actually manages to get in the way of doing stuff. When you’re sixteen indentation levels deep into a complicated expression, it can be hard to remember what you were trying to do five levels ago, and which parentheses match up with which. And call me an uncultured reactionary if you must, but I prefer my arithmetic in infix notation, thank you very much. Perl’s syntax may be terminally funky and riddled with special cases, but those special cases are there to do things that people want to do. Even in PHP, I hate having to write match("([[:alpha:]]?)[[:digit:]]+", $string, $matches); when I could just write $string =~ /([a-z]?)d+/; in Perl.