Yeah. What is up with me and Lisp? Maybe you are a Lisp programmer and you’re interested in why I choose to spend so much time in parentheses. Or maybe you’ve never programmed in Lisp and you’re interested in why someone might choose to.
Well, these days, the question of why a person might choose Lisp is somewhat difficult to definitively answer. Time was, Lisp could make the claim, along with cousins, like Smalltalk, to be among the most forward thinking of programming languages, but contemporary life is littered with languages just as clever, dynamic, ergonomic, or mathematically sound. So while the question is, in many ways, more difficult to answer than ever, it is also more salient than it has ever been1.
(Maybe it’s not the) Metaprogramming
However, the one thing Lisp dialects tend to have in common is the ease with which one can metaprogram, and I think this is a major component of the Lisp experience and this at least seems to be related to their distinctive, fully parenthesized, s-expression based syntaxes. And I would like to advance that it is in the area of syntactic simplicity that Lisp’s true appeal lies, at least to this author.
So what, right? The observation that metaprogramming is the reason to use and/or like Lisp is hardly new. Sure. But what is true is that metaprogramming is now hardly the exclusive domain of Lisp. Contemporary programming languages all support aspects of metaprogramming in one form or another. In Ruby, for instance, blocks give you 70% of what you want out of metaprogramming for 10% of the price you pay in Lisp2, Python uses decorators to expose a limited set of metaprogrammatic techniques, C++ allows psychopaths3 to template metaprogram, language-level support for monads in Haskell constitutes a kind of metaprogramming in that it exposes an interface for the nature of binding in certain contexts4. And if these language features don’t strike you as rich enough to compete with Lisp’s `just mess with the code before the compiler gets it` approach, most languages have these sorts of facilities these days.
So what is it that you can’t get from other languages that you can get in Lisp?
What you get
At the outset, I want to say that I’ve had the luck to work in Common Lisp for the past two years and that, after an initial period of adjustment, I’ve come to see the language as extraordinarily well designed, if lacking, somewhat, in modern sensibilities. So if you choose to work in Common Lisp, you get a very pragmatic, extensively well thought out, extremely powerful base language to work with. As a matter of comparison, Common Lisp is more cleverly and extensibly object oriented than Ruby, with semantics carefully thought out so that most implementations can acheive significantly better performance. So if you have the expertise and the temperment, you can rely on a good Common Lisp implementation to get you where you need to to go.
But that isn’t really the point of this little essay. I’m not primarily interested in the best language for getting a project to completion. If that is your interest, lots of factors will influence your decision that don’t have anything to do with the fact that Lisp might feel like a really well broken in pair of jeans.
Ultimately, I think Lisp’s real strength is the unpretentiousness with which it exposes the underlying nature of the programming system. While the Lisp community might be accused of pretention from time to time, I think the language is decidedly straightforward: in most languages you go through the, in my opinion, silly exercise of denoting a program tree using some large set of syntactic conventions, and then the language runtime converts that into a syntax tree, which is then operated upon to produce executable instructions of some kind. In Lisp, you are able to pretend, at least5, that you are denoting that intermediate tree directly.
This is not profound. On the contrary, it is essentially pragmatic6, and it has the appeal of the essentially pragmatic.
One source of feelings of alienation, in my opinion, is to be expected or forced to act in ways which one knows are meaningless. It might be argued that your average algol-syntaxed language is asking you to do that which is alienating: denote what you know is simply a tree as something which requires a complex transformation to arrive there.
The benefits of this simplicity extend beyond vague feelings of empowerment. They enable, for instance, one to engage in code transformations without the conceptual overhead of source to source transformation. Of course, the runtime is performing a source-to-tree transformation, but it feels trivial, and your own code which operates on the parse tree can, most of the time, be written as though it is operating on the thing you typed, rather than the thing which is produced by the runtime. Once you encompass backquote, unquote, and unquote splice, denoting program transformations requires very little conceptual overhead7.
But the other benefit is the fact that your toolchain benefits in kind from the simplicity of the language. Tools like paredit or the excellent Dr Racket8 further close the gap between typing individual characters and denoting and manipulating, directly, the code tree.
But I do think that Lisp’s simplicity in the arena of “source to parse tree to executable code” gives it a distinctly pleasant, distinctly empowering “feel,” and that probably accounts for a significant number of its deep adherents.
And that is the deal with me and Lisp.
1: That is to say, the similarity is not itself a property of the languages, nor a property of the immediate Lisp community, who somewhat close to the languages in question, tend not to confuse them, but a property of the community of programmers `at large`.
2: Of course Smalltalk did blocks earlier and better.
3: No shortage of these, somehow.
4: A given monad can be thought of as specifying alternative semantics for the act of binding a value to a variable and handling the result of an expression evaluated in the context of that binding. A surprisingly large space of `mini-languages` can be reached by controlling exactly what these two operations constitute.
5: I say pretend because it turns out that your average Lisp reader is really doing a lot of work, and that the data structure you denote and its denotation are, as a consequence, not as close to one another as you might imagine. The key is that they are close enough that you can avoid thinking too hard about it most of the time.
6: Literally. The original intent was to denote Lisp programs using m-expressions, but it was an extra complication that never really caught on, since most programmers found it simpler to just use parenthesized notation.
7: Or so it seems. One shortcoming of Lisp, it might be argued, is that it makes thinking about program transformation seem easier than it actually is. Correct program transformations, in which the resulting code does what you expect it to do in all scenarios is a pretty hard problem. A hard problem which is not even generally conceived of in its entirety by those writing macros, as evidenced by the frequency with which one hears the false claim that `gensym` is sufficient to avoid unexpected behavior.
8: To be fair, Racket goes way beyond Lisp in general in this area.