CommonLispThoughts
This is a list of notes for things in Common Lisp that are notable when considering making a better version of such. So, things that are both bad and good.
A more cheeky but kind of accurate name would be “Why Common Lisp Is Not My Favorite Programming Language”, I guess. :/
This is for Common Lisp as defined, not any particular implementation of it. Often implementations will paper over or fill in gaps. This is part of the problem.
Also see Lisps
Last chapter of hyperspec read: 13, go to http://www.lispworks.com/documentation/HyperSpec/Front/Contents.htm for quick link
The good
Less-than-obvious things here. No need to go into “S-expressions are awsum!”
- Incremental compilation model
- Gradual typing
- Syntactic macros, natch, though they could be made better (hygenic, for instance).
- Reader macros are quite sexy, but are they necessary? Hmmm. They make life hard ’cause you have to start talking in great detail about how the lexer and parser work.
- The conditions system is quite swank.
- Docstrings ye gods
format
is pretty swank- CLOS is really pretty damn sweet.
- The global/dynamic/lexical extent model of thought in scoping, while kind of wonky, is really pretty descriptive.
- The nonlocal exits stuff is quite nice, and very interesting because it happens in dynamic scope, not lexical scope.
- The abstract streams and pathnames, though clunky in modern terms, is pretty reasonable. There ARE some neat combinators for streams as well. Though a disturbing amount of stuff is undefined with them.
- Features are pretty nice! Though defining some standard feature names might be nice, maybe? Or at least prefixes and such.
The bad
- Lisp-2’s are annoying. Even more so when a symbol can actually represent values, functions, classes, itself, module-exports, and on, and on… Symbol property lists are sad. What the heck are they even for?
- Type system and CLOS are sort of awkward partners; both work together but CLOS kind of sits atop normal types. CLOS provides subtyping and such, while you can’t do that in normal types, and you can’t really define new normal types particularly well. Things that could be easily implemented in CLOS, such as pretty-printing definitions for arbitrary types, are not.
- No union types or pattern matching (though of course you can roll your own)
- Born in an age before immutablity was really a thing
- Vectors and hashtables, tbh, are a pain in the butt to use, just in
the number of keystrokes involved. Do I want to do
(setf (gethash a 'foo) 10)
ora[foo] = 10
? - Array fill pointers are a hack (should have a cursor object that wraps an array, or just use stream.)
- Packages are kinda heavyweight, the conceptual model is a bit obtuse
and jibes with symbol-based name resolution… weirdly.
- I’m a fan of having to declare names before they can be used
- CLOS multiple inheritance is probably the wrong way to do things. Its class precedence list basically does a breadth-first search to determine what is actually attached to what and how, which is kind of amazing, but…
with-open-file
and friends can be replaced with generic destructors- There’s a whole pile of junk defining how the REPL behaves that may or may not be necessary. You can tinker with the REPL so much that maybe it is necessary, since really CL has no concept whatsoever of a “noninteractive program”, but it feels icky to me.
- The standard’s definitions of safe vs. unsafe code and unspecified/undefined behavior are a little bit evil. Allowing sloppy definitions like that is often a bad idea because it makes a million special cases and implementation hacks… you know, like the differences between CL implementations.
- Nitpick: Multiple return values are unnecessary if you have tuples
- What the heck are
&aux
argument bindings for? - Yikes there are a lot of type specifiers. …really, it’s that lots of types feel like special cases, instead of “atom, sum type, product type, and combinations thereof” that seems to be able to encompass pretty much everything.
- No list swizzling (a la Python’s
*
operator) outside of macros? So you need bothapply
andfuncall
. Huh. eq
,eql
, andequal
are still kind of awful. And then there’sequalp
which I didn’t even know about. Yeesh.
Legacy stuff
Most of this is an artifact of it being defined in 1981. But the C standard has been updated four times since then, for example.
- No Unicode
- No networking
- Path names are squirrelly and implementation-defined
- Math and numerical stuff is simultaneously baroque and lacking; see
phase
for an example. - Time and timing exists but is frankly understated
- No threads
The ugly
- Names are nonsense.
dbp
,deposit-field
,dribble
,mapcan
,nconc
,boole
,ldb
,pairlis
,prin1
,princ
,proclaim
,declaim
,room
,some
(!!!),sublis
(substitute list) andsubseq
(take a subsequence!!!),terpri
… - Even worse, lots of things have subtly different variants that might
look similar, such as
subst
andsubstitute
. Or are similar but so vague you can’t remember which the hell is which, such asdeclare
anddeclaim
,defvar
anddefparameter
(neither of which really deal with parameters as defined in the hyperspec). Or are similar but look very different, such asdbp
anddeposit-field
. Ugh! - Lots of things which should be composable functions are instead random options that some functions accept (and others do not). Additionally, it’s missing lots of things that are nice combinators, such as fold.
- Types get massively overloaded with functionality and special cases.
Consider the following definition from the hyperspec: “simple vector n.
a vector of type simple-vector, sometimes called a”simple general
vector.” Not all vectors that are simple are simple vectors—only those
that have element type t.” Given that
t
is the top type that contains every other type, including itself… what? Streams really mean both streams and terminals (that is, interactive streams) (and broadcast-streams) (but things get left out too, presumably because shit is so complicated it gets overlooked; there’s string streams which read/write characters to strings in memory, but no binary streams which do bytes into arrays afaict; nopeek-byte
either even thoughpeek-char
has a gazillion different fucking options that alter its behavior in subtle ways).
defconstant
creates a constant that can be overridden. No wonder CL is hard to compile.
The ambivalent
- The image-based environment model is… a little squirrelly. a) it does not map well with how computers generally work nowadays, and b) I tend to distrust things that can sneakily hide state that isn’t nailed down in the definition of a specific file somewhere.
- The compilation and environment model is complicated. This
might be necessary though. But… compiler macros? Four different
environments with different evaluation times?
- Holy crap you can redefine classes on the fly and have all instances of it in existence automatically changed? I’m not sure if that’s awesome or terrifying. The bonkers thing is there’s a hook that lets you turn an old instance into a new one, so you can actually make it totally work.
- setf places are basically generalized rvalues, similar to
operator=
. These things are generally useful, honestly. - In fact CLOS is in general a very featureful and complicated system. Why does it have class variables (shared slots) in a system with good namespacing and global variable control anyway?
- setf is a little weird. It’s nice because it’s basically
operator=
, a general assignment wossname, but while it really supercedes things likearef
andget
and such… it doesn’t replace them. So you end up with many ways of doing the same thing. - Similarly, CLOS really supplants but does not replace structs. (I guess objects have vtable’s and struct’s don’t, but…)
- Hooks, hooks everywhere… this is both amazing and terrifying. You could write pretty much anything and it could go off and do anything without being obvious what in the world is going on.
Closing thoughts
Three main design features seem to be in evidence:
- Can change anything at runtime, which leads to
- extremely dynamic, and
- extremely hook-y.
The first is to some extent a consequence of the image-based model, since lots of things you want to be able to change immediately and be changed immediately, instead of changing a source file and reloading the system. (Or vice versa; cause and effect are uncertain here.) In my feelings, the extreme dynamicness might lead to having a hard time specifying things; if you could define a data structure but it’s simpler to just use a list, it feels like it’s hard to end up with much abstraction. Plus, it’s very easy to change how the system works, if not downright scary, which makes systems hard to understand, which is dangerous as well. I outright dislike the hook-y-ness, since it makes discoverability hard and it means that lots of things that should be done with well-chosen primitives and combinators are instead just slapdash piles of hooks everywhere. This, again, makes it hard to build good designs that are easily generalizeable.
I dunno. Maybe this was valid and useful when it was possible for someone to understand in depth how an entire system works, but these days nobody’s got time for that.
Also, a lot of the time this defines an actual operating system, to a
large extent, including things like fairly low-level I/O and UI, such as
y-or-n-p
. But it’s all very poorly modularized and so,
again, hard to combine or distribute. Nobody needs their microcontroller
to implement y-or-n-p
and pretty-print
.
Extension: Clojure
So the best descendents of Common Lisp are probably Scheme and Clojure right now. I know scheme, let’s learn Clojure and compare.
Initial thoughts
Starting by reading https://aphyr.com/posts/301-clojure-from-the-ground-up-welcome
- It takes 7 seconds to start up a repl on a core i5 laptop with an SSD. WTF is it DOING, anyway? SBCL takes half a second. Stupid JVM.
- oh thank gods, real boolean types. …sort of. nil and false are falsy, everything else is truthy. But nil does not appear to be the same as an empty list. What’s the point of nil then? Is it just an empty collection of any type?
- Equality is still a little loosey-goosey.
(= 3 3.0)
is false,(== 3 3.0)
is true. Not sure how wobbly that is yet. Looks like the former is equivalence, the latter is numerical equality. - Math identities! Neat! And
(<= 1 2 3)
is true. Turns out both those are inherited from common lisp. - Shortcut syntax for defining vectors! And indexing them! And sets! And maps! Glorious!
- Vectors are maybe actually btree’s? Well, if it’s all-immutable, that makes sense-ish.
- Okay, mutability is through explicit cell types,
Var
. Huh. doc
,source
,meta
, all sorts of shit… awesomesauce.- take, drop, repeat, range, map, all sorts of combinators…
- oh look, monads. with macros! Yay.
- Macros seem hygenic.
- Ooh. Oooooooh. This is just Haskell with parenthesis!
- Shiny concurrency stuff as primitives! Like… a million of them that do different things. ’Cause concurrency is hard even when you pretend otherwise
- lein: Nice project thingy, like cargo
- all symbols have namespaces; looks a lot like common lisp except explained better
All in all: Pretty nice. Shame about the Java thing.