The beauty of Common Lisp

It's often said that Common Lisp is ugly. I don't necessarily disagree, but in this short essay I want to briefly defend the beauty of Common Lisp, precisely in the places where we usually find it most ugly.

Lisp-2

One of the reasons most often cited for Common Lisp being ugly is that it is a Lisp-2 – namely, a Lisp where symbols don't have only one universal meaning, but two separate meanings, depending on where they appear in a form: at the car of a form, where whatever value the symbol holds will be executed as a function with the rest of the form as arguments, the symbol's function-value is used to determine what it means; anywhere else in a form (or when the symbol is by itself), the symbol is looked up in the lexical and dynamic scope to determine its value as a variable.

There are two reasons this is often found to be ugly:

  1. Theoretical reasons: the distinction between function and variable values is found to be arbitrary – an unfortunate exposal of an underlying optimization that the user of the language shouldn't have to see. If functions are first-class values, just like any other value in the language, as they should be in a functional language like Lisp, then we should just store functions as regular values in the value cell of a symbol, and just look in the value cell of a symbol when we want to use it as a function. This seems more consistent.
  2. Ergonomic reasons: this two namespace system makes higher order functions more awkward, because you have to worry about the distinction between the two values of a symbol, and while values can't ever be assigned to the function cell, you can regularly get function's assigned to the "wrong" cell while using higher order functions, which discourages functional programming, because then if you accidentally get a function storied in the variable, instead of (my-variable) you have to do (funcall my-variable), and if you want to get the function value that's stored in a symbol properly, instead of (another-function a-function) you have to do (another-function #'a-function).

However, I think there's a certain beauty to making this distinction: since the semantics of a symbol – the way in which it will be actually used – differ based on where it appears in a form, it makes sense for its sense to change to something applicable to how it will be used as well, just like how a word in English that appears in a verb position won't use the same definition as it would if it appeared in a subject or object position, but a different one, because it will be used differently. So when a symbol in a Lisp-2 is in a position where it will be used as a function, its meaning as a function is used, and when it appears as a variable, its meaning as a variable is applied. This also helps guarantee that we're always calling a valid function, since calling some other value is never valid, even in Lisp-1s, since only functions can be assigned to the function value of a symbol.

It's not just that it makes sense in an abstract way, either – the commonly cited benefit of Lisp-2s falls right out of this. Just as, in a natural language that didn't allow words to have different senses depending on where a word appeared, you'd need double the words, in a language where each symbol can only have one meaning, even if it appears in a totally different place and will be used in a totally different way, you suddenly have to worry about name collisions even in totally different semantic contexts from where the other use of the name exists, which forces you to think more about things and use tons of abbreviations and synonyms. Why would you want to have to deal with that for the relatively-rare case where you actually do what to use a variable as a function?

We can demonstrate that having names mean different things in different contexts (where the meaning will be used differently) is useful by just thinking about other examples, as well. For instance, imagine if you could accidentally clobber a module name with a variable name! Or, think of all the minor annoyances that could be resolved in many languages if variables didn't have to share a namespace with types and keywords, so that you could name a variable "class" or "int" if you wanted to. Even in Scheme, the poster child for Lisp-1s, module and macro names live in a different namespace than variable names to avoid clashes!

There are many other technical reasons that one might prefer a Lisp-2, at least at the implementation level, but one can always argue that those technical implementation issues shouldn't be exposed to the user, so I don't personally find them quite as interesting for their own sake as the aforementioned considerations.

(Plus, with sufficiently powerful macros, all language changes are possible)

Does this mean I think Lisp-2s are actually better? No, not really. But it's worth thinking about.

Multiple equality operators

One of the other things people seem to find most ugly about Common Lisp is the fact that it has multiple equality and copy functions, instead of having a single generic one. The reasons behind this are solid, however: what kind of equality or copy operation the programmer wants depends on the semantic intent behind a type, which can't necessarily be determined from the type itself. Thus if only one copy and equality operator can be provided, the choices are either providing only one type of equality/copying and leaving it totally up to the programmer to implement the rest, or providing generic ones that try to guess what the programmer wants based on the type and hope you get it right, and if it isn't the right test, they're just out of luck. (For a more detailed discussion of this, see this post by Kent Pitman.)

This could of course be solved by copious usage of newtypes and custom algebraic datatypes, for which you implement custom equality and copying methods, which seems to be how ML-family language programmers solve it, but this has its own downsides. First of all because overly structured data adds verbosity and lacks flexibility; and secondarily because this requires you to implement your own equality checks over and over again; and finally because it means that you can't make contextual decisions on what sort of equality check to make without introducing your own zoo of equality checking predicates.

Instead, what Common Lisp decided to do was take all of the copying and quality operators that all the different dialects of Lisp had discovered were useful and ended up using in the real world, and formalize them into a strict hierarchy of equality checking, from most to least strict, so that programmers themselves can choose up front what kind of operation they want to use in each case.

The ultimate postmodern programming language

Notes on Postmodern Programming, which I otherwise enjoy, claims that Common Lisp is a modernist programming language. While there are certainly ways in which that's true, namely that it enforces S-expressions as the universal syntax, and the fact that it's standardized, I believe that these things actually only exist to facilitate Common Lisp's perfection as a postmodern programming language. Here are the reasons I think it qualifies as postmodern:

  1. The language has no modernist narrative into which all programming must be fit. Instead it comes with a rich set of capabilities for solving the problem using any set of concepts that will apply, often richer and better capabilities than most languages that choose only that one capability. See for instance the co-existence of low level and high level, of procedural, functional, and object-oriented, and of dynamic typing with gradual static typing.
  2. The language is literally unmatched in its ability to allow you to mold it to the problem domain. It does not enforce any limitations – even S-expression syntax, thanks to reader macros – on you at all. You can grow and change and develop it to express any kind of way of talking about a problem at hand that you need.
  3. The language is pluralist: all the ways of molding the language to your needs and thought processes and specific context are just language constructs that can be package namespaced, imported as libraries, mixed and matched, aliased, turned off and on. Whatever modifications you use in one part of a program need not apply to another part, and the dialect that another programmer uses in a library you depend on uses does not effect you, nor does the features anyone wants, because they don't have to be added to the language standard, those who want them can have them and those who don't don't have to worry about them.
  4. The language was not designed up front, absent experience and response to human needs, in the abstract pursuit of beauty and perfection, like Scheme was. Instead it was designed as a way to encode the practical, real world industry experience and needs of real people doing real things. As such its design itself doesn't reflect the sort of modernist attempt to construct and control reality of most languages, but a more postmodernist "writing down of people's experiences."
  5. The language standard only allows the language to be more pluralist than it would otherwise be: everyone can use their own favorite language extensions and domain specific languages and so on, but there is always a common, well understood bedrock beneath it all that everyone can always rely on and communicate using, which means everying is only more interoperable, which is needed for healthy bustling pluralism instead of dead fragmentation and subsequent stagnation. This also allows multiple implementations of the language to flourish as, at least ostensible, equals, where most languages are limited to only one implementation, and all those implementations can live in different places and make different tradeoffs.
  6. Likewise, the simplicity and uniformity of the S-expression syntax is not only just a suggestion – something that can be broken of if you so desire – but is what directly enables the language to be as malleable as it is.

Cons cells

Xah Lee often complains that the cons cell is a fundamental mistake of Lisp. The Racket language hides it from the student version of their language, referring to it as merely an "accident of history." Clojure doesn't have cons cells at all.

From one point of view, this makes sense – all they are is a building block for linked lists, namely a struct holding a pointer to a piece of data, and a pointer to the next piece of data – so exposing them directly to the user to the level that Lisp does is simply unnecessary. Yes, linked lists themselves are deeply useful data structures for a functional language, but the building block they're implemented out of doesn't need to be exposed, and the equivalence of right-nested cons cells and a list means that one can seemingly (for those who don't understand these data structures, usually those coming form other languages) "magically" turn into the other without warning.

However, I actually think this is a slightly misguided way of looking at it. Cons cells are not "just a linked list building block" that have gotten accidentally exposed. The cons cell actually does some pretty nice things! Sure, other languages have better options now, but the key is that you shouldn't get rid of cons cells until you actually include those other nice things. And at least one of these doesn't actually have an alternative.

  1. By having the concept of cons cells natively in the language means that you can actually manipulate how linked lists are structure directly, such as swapping out a new head or tail for a list. This means you can essentially use lists as a persistent data structure if you want to. Clojure of course has superseded this by making everything persistent automatically, with no work on the part of the programmer, but other languages don't have that.
  2. Cons cells are also just useful data structure building blocks in general, since you can actually control where the pointers in it point, and lists are composed of them and you have first-class access to them even in lists, so you can take lists and build new data structures out of them. You can make binary trees with them, without the added layer of indirection of using lists. You can also use them to build circular lists and queues, which would be much more difficult in a high level language without the ability to unzip linked lists and modify where their pointers are pointing.
  3. Being able to represent the general idea of "these two (pointed-to) objects are related to each other" in an efficient way is very useful. In this sense, though, they're like inferior tuples.

Property lists

One of the things that Scheme specifically removed from Lisp was the idea that every symbol has a property list attached to it. This was done because the idea that mutable data could live on something like a symbol, which was supposed to be this atomic self-evaluating data structure, seemed like an impurity to the creators of Scheme.

I don't think this is the case. Being able to attach properties to symbols means that you have an automatic way to create, essentially, a proper-named entity referred to at a first class level in your source code that can have metadata attached to it. While that's not going to be useful to most programs, it seems like exactly the kind of thing that would make using a programming language as a tool for thought more useful. Instead of having to create a hash map registry of entities and then use a getter to access them, you could just refer to things by name, directly, and have meaningful information attached to that.