Which Lisp Should I Learn?
Fri Jan 24, 2014I don't know why this keeps coming up lately, but it does. So, here we go:
My Recommendation
If you want to learn your first Lisp and already know something about the JVM|1|, you should learn Clojure|2|. Otherwise, you should learn Scheme. Specifically, I recommend going the route that takes you through Racket|3|, and possibly through SICP or HTDP.
If you absolutely, positively must, I guess go ahead and learn Common Lisp.
Why
Clojure the language, as opposed to the current, main, half-fused-with-JVM implementation, is cleaner and more consistent than Common Lisp, which should help you learn it more easily. I've gotten back talk about how there are lots more noobs learning Clojure, and as a result their libraries are in some disarray, and about the fact that the JVM is a sack of donkey balls you have to bite into every time you hit some sort of error|4|, and about the general Clojure community pre-disposition to fashion trends. All of which may or may not be true, but I'm specifically talking about the language, not its ecosystem or stalwarts. Now granted, all of Racket, Clojure and Common Lisp are
- built out of s-expressions
- have
defmacro
so depending on how much work you're willing to put in, you can do whatever the fuck you want in all of them|5|. However, in addition to knowing about prefix notation, and macros, and general Lisp program structure, here's an incomplete list of idiosyncrasies of Common Lisp that you have to commit to memory before you can be effective in it:
- Functions and variables are in different namespaces, and each has constructs that deal with them explicitly. You need to use
defvar
/defun
andlet
/flet
depending on whether you're using functions or variables. If you're passing symbol names around, symbols that denote variables can be sent around as'foo
whereas symbols that denote functions should be sent around as#'foo
. If you're trying to pass a function around in the variablefoo
, you need to(funcall foo arg)
or possibly(apply foo args)
, rather than just(foo arg)
. - Most functions that deal with lists are functional, except the standard
sort
and the defaultmapcan
, both of which destructively modify the list you pass them. - You can define methods for your classes, but can't easily use certain default names that are bound to top-level functions. Such as
length
, or the arithmetic primitives. Which is why you frequently see methods likeduration-add
ormatrix-mult
. - There are 7 commonly used equality operators,
eq
,eql
,equal
,equalp
,string=
,char=
and=
|6|, each of which has mildly different, sometimes implementation-specific, behavior. Granted, because CL isn't a pure language, you need at least 2 of those, but 7 is still a bit much to have people memorize. - There are three different local binding mechanisms that you must decide between depending on whether you want to be able to refer to earlier symbols in the same binding set, or whether you want symbols to be able to refer to themselves. You use
let
/flet
if you don't care,let*
for variables where you want later bindings to be able to refer to earlier ones, andlabels
for functions where you want bindings to be able to refer to themselves or earlier bindings. - There are many, many implementations of Common Lisp. The popular ones at the moment are SBCL and CCL, but I've personally seen CLISP, ECL and Lispworks around too. More are available, and you might run into a lot of them in the wild. If you want to write portable code, you have to jump through some hoops. The implementation-specifics range from the finer points of equality operator behaviors, to the behavior of
handler-case
|7|, to the types you can specialize on withdefmethod
, to the presence and behavior of threads, to the contexts in which you can pass streams around, to the names of various extension functions. For a small but representative example, take a look at what it takes to temporarily change your current directory in an implementation-independent way. - Indexing into different constructs takes different forms. You need
nth
for lists,aref
for arrays and vectors,gethash
for hashes andslot-value
for CLOS instances|8|. - You can't use the symbol
t
anywhere. No, not even local scopes. If you try, you'll get warnings or errors|9|, becauset
is the top-level designated Truth symbol, even though anything other thanNIL
evaluates truthily in a boolean context. - A hash isn't a list, and a CLOS instance isn't anything like either. One way you'd like them to be similar is when you're traversing them. It seems fairly reasonable to expect
map
-likes to operate on hashes by treating them as a sequence of key/value pairs, and instances by treating their bound slots as key/value pairs. This is not how things work. If you want to map over instances that way, you need to do something like this and this. If you want tomap
over hashes, you either use the hilariously mis-namedmaphash
|10| or some idiosyncratic piece of theloop
DSL that lets you iterate over hash keys and hash values. - Common Lisp is case-insensitive. It takes whatever symbol input from you and upcases it internally. So
foo-bar
andfoo-Bar
both becomeFOO-BAR
behind the scenes. This is usually not a huge problem, unless you try to interoperate cleanly with newer data standards like CSS or JSON. That leaves you fumbling withstring
s in situations wheresymbol
s and/orkeyword
s really ought to do.
Like I said, this is a small sample. Just the stuff I thought of off the top of my head. I'm sure I could come up with more if I put a day or two into it. And I'm far from the most experienced Lisper out there, others would have more finer points for you, I'm sure. But that's half the problem with little issues like this; experienced Lispers completely forget about them. It's the newbs that have trouble cramming these things into their heads.
When I take a good look at that list, and then imagine the situations that led to each element, it's difficult to conclude that a wrong decision was made at any given point in time. Unfortunately, the sum of all of those potentially correct decisions is a giant system, the inherent rules of which look inconsistent if not outright hostile to human minds.
I don't know if Clojure solves all of them.
I've done very little work with it, for reasons entirely unrelated to the language. For all I know, when you get deep enough into it, you get to inconsistencies and/or restrictions which are worse than anything I've pointed out or could. Also, just in the interests of clarity, in case the "Common Lisp" sitting comfortably at the top of this blogs' tag list wasn't enough of a hint, I use Common Lisp. I like Common Lisp. But it's primarily because I've internalized enough minutia to feel comfortable in it.
But do me a favor, if you're a CL user, either hop over to this web REPL, or install leiningen
then hop into your local lein repl
and type along here:
user=> (def thing [8 7 6 5 4 3 2 1])
#'user/thing
user=> (thing 0)
8
user=> (thing 3)
5
user=>(map (fn [n] (+ 3 n)) thing)
(11 10 9 8 7 6 5 4)
user=> (def thing {:a 1 :b 2 :c 3})
#'user/thing
user=> (thing :c)
3
user=> (thing :d 6)
6
user=> (thing :a 6)
1
user=> (map (fn [[k v]] (+ v 2)) thing)
(3 4 5)
user=> (def thing #{1 2 3 4 5}) ;; a set, in case you were wondering
#'user/thing
user=> (thing 3)
3
user=> (thing 0)
nil
user=> (map (fn [a] (+ a 2)) thing)
(3 4 5 6 7)
user=> (def triple (fn [a] (* a 3)))
#'user/triple
user=> (triple 4)
12
user=> (map triple thing)
(3 6 9 12 15)
user=> (map (fn [a] (let [t (- (triple a) 5)] (* 2 t))) thing)
(-4 2 8 14 20)
Now think about how you would go about explaining to a novice programmer that it has to be more complicated than that.
Footnotes
1 - |back| - And don't have a strong dislike for it, obviously.
2 - |back| - Install it through Leiningen, which is available in the Debian repos in stable and unstable.
3 - |back| - Yes, I'm fully aware that the Racket guys are trying to push this "We're totally not Scheme" thing. They're close enough from an external perspective. Just don't tell Jay McCarthy I said so.
4 - |back| - Which is certainly true, but mildly preferable to the alternative as long as you're used to that sort of thing.
5 - |back| - Except that Clojure is apparently missing Reader macros, which I always thought were kind of half-assedly implemented in Common Lisp. For what I consider the full-ass version, take a look at how Haskell does it.
6 - |back| - Plus how many ever *-equal
functions you define for your own classes.
7 - |back| - The Common Lisp answer to the problems that call for try
/catch
in other languages.
8 - |back| - For the last, you can also define your own selectors using :reader
or :accessor
declarations.
9 - |back| - Which specific warning or error depends on implementation.
10 - |back| - Because it's not very much like map
. It returns nil and works by side-effect. Meaning that if you expect a sequence from it, you'll need to construct it yourself.