I used to see Lisp code and wonder, how could anyone read that? But instead, what I should have asked is: why would anyone write that? Because, as I’ve come to learn, developers who read lisp do so for the same reason they write it: it’s easy to think in.
It’s ironic that this is the main appeal of Lisp. Its syntax is so unlike written English, which is what other programming languages try to model, that when you first read Lisp code, and glance over a file filled with heavily indented, terse statements wrapped in parenthesis, all your mind can think is, “what the hell is this?”
But you should cherish this initial reaction to Lisp, because it sits at the core of what makes it enlightening to learn: it doesn’t model other languages for the sake of familiarity; instead, Lisp is the result of thinking from first principles. And I don’t say that in an Elon Musk change-the-world type of way. Lisp, in fact, was proposed by John McCarthy in his 1960 paper as a formal model of computation, before it was implemented by his student, Steve Russel, as a full on programming language.
So what principles is Lisp based on?
I’d like to explore this question deductively, the same way I came to understand it. While learning Lisp, I’d wonder why its syntax had to be the way it is. The way you add two variables, for example, is
(+ x y), why can’t it be
(x + y) or even better
x + y. Likewise, with conditional expressions, instead of
(and x y) why can’t it be
(x and y) or even better
x and y?
These are in fact the two most glaring questions one has when learning Lisp:
Why are there so many parenthesis?
Why are operators out of their usual order?
As it turns out, the underlying answer to these two questions is the same.
Let’s start with parenthesis. Why must Lisp have so many if other programming languages manage with less?
Well, even that’s not necessarily true. Other programming languages have the same amount of parenthesis, they’re just situated differently.
if (x) foo() else bar()
(if x (foo) (bar))
if its condition is wrapped in parenthesis; that next
else statement has to be in a new line (that is, unless you surround each expression block in curly brackets); and finally, to call a function you place parenthesis next to it. Whereas in Clojure you only need to know one rule: you place parenthesis around expressions in the order you want them to be evaluated.
You see, parenthesis are essential to Lisp for the same reason their essential to math: they emphasize the order of expressions. Would you rather memorize arbitrary rules like PEMDAS (what’s
2 + 3 + 4 * 5?) or have one visual indication for the order in which expressions should run (what’s
(2 + 3 + (4 * 5))?).
condition ? foo() : bar()
That leaves us with two parenthesis rather than three. But it’s one more arbitrary rule we have to know for the sake of nicer syntax.
You might still be wondering: couldn’t we preserve the order of expression with something else, like new line characters?
Let’s use a more complex example, a factorial program, to see what that might look like:
(defn factorial [n] (if (= n 1) n (* n (factorial (dec n)))))
defn factorial [n] if = n 1 n * n factorial dec n
The first example is 4 lines long, the second one is 7. In the first example, the nested parenthesis make the code easier to follow, but with all of them jumbled up, they also make you want to read it less. But here’s what I didn’t realize about parenthesis before writing Lisp: there’s wonderful editor support for them. From colorizing them based on their nesting to connecting them with a line as you focus on their code block. So you get more control over the structure of your code, without the trade-offs you’d visually expect.
With Lisp, as you learn why its syntax is the way it is, your conception of nice syntax changes. The same way vegetables taste better once you associate their taste with health, parenthesis read better once you associate their presence with simplicity, and install a plugin for vscode that makes them visually appealing to look at.
(why? '(first args))
Along with considering the order in which expressions are run, we also have to consider the order in which operations and their arguments are listed.
x + y + z
(+ x y z)
For most developers the first example is clearer since the plus sign is in-between the variables, just like it’d be in regular math. Whereas in the second example the plus sign is first and the variables are after. That’s a consistent pattern in Clojure and other Lisp dialects because putting the operation first allows you to pass multiple arguments to it; and that’s something you expect to do with functions. Which is in fact what algebraic operations like
+ are in Clojure: functions.
All programming languages have special forms. A set of primitives the compiler needs to know how to specifically interpret, and thus special rules that you as a developer have to know. What’s special about Clojure is that the only forms that are special are the ones that can’t help but be it–and there are less than 20 of them; because Clojure, as a Lisp, builds on top of itself.
if statement, for example, is a special form because the compiler needs to be instructed to conditionally evaluate an expression; the
when statement, on the other hand, doesn’t have to be because it can be built of top of
In Lisp, the special form that lets you to build on top of the language itself is called a
macro. You might ask, why doesn’t my programming language have macros? But a better question is: if my language had macros, how easy would they be to use?
In other programming languages syntax is more complex. Not only because it’s less uniform, but because code isn’t represented the way it needs to be evaluated. While other programming languages toss around abstract syntax trees, in Lisp, your code is already in the shape it has to be: a list of nested trees.
That’s in fact where the name “Lisp” originates. It’s a list processing language, built for the purpose of transforming one list of code trees to another.
Because of Lisp’s syntax, code can unfold code in your mind the same way it’s unfolded by the compiler: recursively. No need to walk an entire tree of code and add special hooks to transform it. With Lisp macros, you can compose code transformations as standalone functions, and as you do so, you only need to handle the level of the code tree you’re already thinking about.
You see, what’s special about Lisp isn’t that it has macros, but that its syntax is simple so you actually use them.
I’ll happily admit, I couldn’t help but smile while learning Lisp. In the moments when I realized a programming language could be built around such a small set of rules and that such rules could be so logically transparent. I had lots of questions about why Lisp’s syntax is the way it is, and while my parent languages just said “Because,” Lisp came around and explained itself to me. As Lispers like to say, “code is data, and data is code.”