Okay, it's been a bit more than 5 days. I first picked up Common Lisp about 4 months ago because I was searching for new programming languages and heard a lot about how Clojure wasn't a "real" Lisp.
Common Lisp is one of the most popular Lisp dialects, alongside Scheme, Racket and Clojure. It is also very old: the first version of it was in 1984 and the final version of it was in 1994. For context, Java was first released in 1995. As a result, a lot of inspiration has been taken from it when developing newer programming languages, and Common Lisp has not moved at all. This means that many things I now take for granted in programming languages, especially Clojure, were either pioneered by Common Lisp (with the resulting jank) or nonexistent.
Still, I enjoyed writing Clojure and wanted to try something not tied to the JVM, so Common Lisp it was. Common Lisp also has reader macros (which Clojure does not), which means that literally any syntax can become valid Common Lisp. That seemed appropriately terrifying.
After about a week, I learned enough to solve Advent of Code 2016 day 15. Because I already learn a new language every year using AoC, it made sense to use use previous events to learn languages on the off-season. Unfortunately, my program was quite clunky and I had tried to learn how to use Emacs at the same time, so it ended up not being very enjoyable. I moved onto Rust and Scala, and left Common Lisp by the wayside.
Fast forward to about 5 days ago, when I had a bit of extra time on my hands and saw AoC looming on the horizon. After about 60 seconds of fiddling with Clojure and finding out that Zed didn't have an integrated REPL for it, I decided to try Common Lisp again. Lo and behold, it turns out revisiting an old topic after ramming your head against it months ago really does make it easier.
This really has only been 5 days since then, so my thoughts are likely to change in the future. I just wanted to get some of these thoughts down while I'm still very much a novice with the language.
Development Environment
I decided to learn Emacs because, overwhelmingly, those that promoted Common Lisp said that the development experience was unparalleled specifically with Emacs. This is because of SLIME, which is essentially an integrated REPL. I'm sure there are lots of other features that I haven't discovered yet. I had learned vim several years ago, but only enough to do work in it. Despite my great typing speed, I still prefer graphical editors and doing things with a mouse and arrow keys. Maybe Emacs would be more intuitive?
Well... kind of. The single greatest benefit Emacs has over vim is that you don't need to enter a different mode to start typing. The main thing a text editor does is edit text, so it only makes sense to make it the default mode. Not much has stuck from Emacs tutorials, so I think I still prefer the modern shortcuts in VS Code/Sublime Text/Zed for most things.
Originally, I used Portacle which advertised being an all-in-one solution, but I later found out that it's been unmaintained for ~5 years and is slowly going out of date. Now I use emacs4cl, which is one .emacs file that sets up for programming in Common Lisp. I have Emacs and SBCL installed separately from apt. Aside, SBCL appears to be the main compiler for "normal" Common Lisp development, and I haven't had many problems with it.
Emacs
A braindump of all the commands I can remember off the top of my head (and are thus the ones I use enough to be worth remembering):
C- means hold Ctrl, M- means hold Alt.
- Open/Create a file with
C-x C-f(eXecute File). - Save a file with
C-x C-s(eXecute Save). - Open SLIME with
M-x slime. -
Cancel a half-typed command with
C-g. -
Minimize your current window with
C-x 0. - Maximize your current window with
C-x 1. - Kill a buffer with
C-x k. -
Swap buffers in a window with
C-x b. -
Start highlighting with
C-SPACE. - Highlight current s-exp with
C-M-SPACE. - Cut highlight with
C-w. - Copy highlight with
M-w. - Paste with
C-y(Yank). -
Comment the highlighted section with
C-;. -
Delete to the end of a line with
C-k(Kill). - Undo with
C-/(supports a ton of undoing).
You can edit by typing directly into the buffer.
- Compile the current function (send it to SLIME) with
C-c C-c(Compile). - Compile the current file with
C-c C-k(Kompile??). - Execute the current s-exp with
C-x C-e(eXecute Expr).
My Emacs has come with paredit, a tool that automatically balances parentheses for you. For example, typing an opening paren automatically types a closing one, and deleting an opening paren automatically deletes the closing one if there is nothing inside. I was initially quite frustrated because I often delete single parens while reformatting code, but it turns out that I just needed to learn the right shortcuts.
- "Slurp" the sexp to the left or right with
C-(orC-). - "Barf" the sexp to the left or right with
C-{orC-}.
For example, barf-right will modify (a b c) into (a b) c. You can also add an extra layer of parens by typing () a b and then slurping-right twice. If you need to get rid of the parens, instead of barfing everything, you can:
- "Splice" an extra layer of sexps with
M-s.
This will change ((a b)) to (a b).
These shortcuts have made paredit much more friendly, and it's great that I never need to type ))))) manually at the end of functions anymore. It's still annoying if you're trying to write a single paren in a string though.
Common Lisp
On the whole, it's not too different from Clojure. Mainly, everything is parentheses now, and a lot of things have either long or obscure function names. It feels like lots of ways to do things that range from normal to absurdly powerful.
The main reason Clojure is "not" a Lisp is because it doesn't have cons cells, which is Lisp's version of a linked list. A cons cell is essentially a pair, with the first element "car" storing data and and the second element "cdr" pointing to the next element. A list ends with nil.
You build a cons cell with (cons 4 nil), which creates a list (4). (car list) is thus equivalent to (first list), and (cdr list) is equivalent to (rest list). Common Lisp has a great (?) way of dealing with nested lists: the standard library contains a huge number of combined car and cdr calls. For example, getting list[1][0] would be (car (cdr list)), but you can rewrite that as (cadr list). You can chain these up to 6, I think, so you can write (caaddadr list) if you really wanted?!1
In "nice" languages (see Clojure, Scala, JavaScript, Haskell), you can "destructure" objects and bind them to local variables at the same time. This is extremely common in my code. For example:
(let [[a b] pair ...)
const [a, b] = pair;
Common Lisp does have this, but it's behind the incredibly long function name destructuring-bind. If only screen width was free real estate.
(destructuring-bind (a b) pair ...)
Common Lisp also has an absolutely insane loop macro. You can write
(loop for s = (list circle final size circle 0) then (take-across-present s)
when (= (third s) 1)
return (caaar s))
and it will work. I'm scared to imagine what the source code for this macro is. However, it is a pretty neat alternative to the collection functions like map and fold.
There are also FIVE equality operators: =, eq, eql, equal, and equalp. I'm starting to learn the differences between them, but I thought JavaScript was already crazy for having TWO equality operators.
SLIME
I'm probably just scratching the surface of what I can do, but being able to send functions directly to the REPL is already great. No more typing (:l "day16.hs") over and over again!
One cool thing is that you can call functions with undefined functions, and SLIME will send you into the debugger. You can then supply a custom return value right there, and then it'll continue like nothing happened. It's quite similar to how you might stub a function during a test. Honestly I haven't used it much, but it sounds nice for when writing a general function without having all of the component functions yet.
Another cool thing is that you can arbitrarily interrupt the running program with C-c C-c. It sends you into the debugger, and lets inspect the current call stack and values of variables. I don't use a debugger normally2, so maybe this is already possible in other languages. But especially during Advent of Code I'm prone to writing code that will take years to complete, and it's nice to be able to inspect certain variables on the spot to check whether I should let it keep running or to rewrite it.
- Navigate through history of commands with
C-↑/C-↓. - The most recent result is stored in
*, the 2nd most recent is**.
The SLIME REPL is also an Emacs buffer, so you can use all Emacs commands in it (like for editing s-exps, or copy/pasting).
One thing I haven't figured out is a nice way to restart SLIME after it runs out of memory and sends me to LDB. I end up having to kill the *inferior-lisp* process, close the relevant buffers, and restart SLIME using M-x slime. None of what I said above makes sense to me either.
Other Thoughts
Common Lisp is fast. It's a compiled language so it's not that unusual, but I was pleasantly surprised with how fast my AoC solutions have been running. Clojure does not have a reputation for being fast.
I explored a few utility libraries like Alexandria and Serapeum, but they don't really seem to improve the syntax much. rutils has a lot of what I'd like to see, but I think I want to tough it out with the standard library a bit longer. These abstractions can add runtime costs, and I can always write my own tools once I become proficient enough.
I feel pretty certain that I'll be using Common Lisp for this year's Advent of Code, which starts in 9 days! Hopefully I learn a few new things.