I’ve been meaning to write this post for a couple of weeks now.
There is a story told among programming language enthusiasts that
programming as an art only “clicks” once a programmer understands
the Lisp programming language.
I finally feel like I’ve reached that point.
Although I don’t think I’m an amazing
programmer, I finally feel like I understand the difference between
languages (like Python and Lisp), and why Lisp is often considered
so much more flexible
and powerful (at least in theory) than a language like Python or C.
(For reference, see any of Paul Graham’s somewhat self-assured arguments
about Lisp. I like his Blub essay the most.).
Reserved Keywords vs. Provided Functions
Take, for example, a for loop in Python:
for x in range(10):
yield x
In Python, for is implemented as a reserved word. That means that a
programmer cannot name a function or variable for. And although this is
reasonable in the case of for,
there are actually 31 whole reserved keywords in Python 2 by my count!
The implication of this is that any developer who wants to hack on the Python
language (by e.g. programatically rewriting Python source code)
needs to be wary of those 31 different restricted Python
symbols which cannot be customized at all. This challenge is further
complicated by how most of the keywords react with the other keywords in
complex ways.
break interacts with looping constructs for and whilecontinue interacts with looping constructs for and whileelif can only follow an if blockelse can only follow an if, try, or for blockprint is a statement and not a function in Python 2def creates a function and binds it to a name.
It can be defined within any scope a variable can be assigned,
and has interactions with return and yieldyield permutes the enclosing function block to become a generator
function, rather than a traditional functionreturn will immediately exit any function scope it is nested within,
including within for and while loops. However, return cannot
be invoked at the top level of a file, because there is no valid enclosing
function scope.
In general, it is nontrivial to rewrite programs
in most programming languages
(which are not Lisps) because the keywords have
complex interactions and behavior. The only
real solution is to introduce a parser for
the language in question.
Compare Python’s 27 keywords with the 15 or so keywords in Clojure
(many of which are targeted for Java interoperation)! The number of keywords
is less than half, and in fact, there are far fewer keyword interactions than
there would be in Python (or Java, C++, etc.). The essential keywords in
Clojure for defining the base of the language (excluding the Java interop bits)
are:
def binds a value to a symbolfn creates a functionif creates a ternary blockdo evaluates a sequence of formslet temporarily binds values to symbols in a scopeloop is like let but also creates a point of recursion for use with
recurrecur evaluates its forms and then attempts to jump back to the immediate
enclosing fn or loop point
That’s it. That’s all it takes to define the language semantics of a Lisp.
Notice that 5 of the 7 special keywords in Clojure don’t even care about the
other keywords. loop and recur have special semantics with each other, but
otherwise, there are far fewer global, arbitrary keywords to memorize.
How does a for loop look like in Clojure, then? See for yourself.
Like with Python, for takes a sequence as its argument(s), in this case x.
It can be read as if it says “for x in range 10”, although it
doesn’t read quite as fluently as Python tends to. However, note that unlike
Python, Clojure does not need to treat for as the beginning of a block, and
for does not require the presence of a reserved in keyword either.
In fact, unlike in Python,
Clojure implements its for as a macro
which rewrites the sequence iteration as a recursive traversal, and accumulates
the results into an output. While Python’s for keyword needs to be
implemented within the guts of the language, Clojure’s for macro is provided
solely as a convenience: you could have written it yourself.
Homoiconicity and You
The appeal of Lisp is that its program have the property of
homoiconicity, which means that
Lisp program representations are structurally and semantically identical to
how Lisp code is literally structured.
(This is the farthest thing from the case in all other programming languages.
Interpreting/compiling other languages requires lexing words in the language
into tokens and then parsing the stream of tokens for meaning within the
context of the language.)
We can
ignore some of the more powerful implications of homoiconicity to start,
and only focus on the clean and simple interpretation.
All Lisp code looks the same;
all Lisp code follows the same rules;
all Lisp code follows a predictable pattern.
Unlike in most languages, which have special magical keywords and
features (I’m looking at you, Python
list comprehensions), Lisp has nothing
fancy and needs nothing fancy: only parenthesis.