There was this question on how to design Lisp software on r/lisp. The answers mostly falling into one of
The last response is quite close to acceptable architecture advice. But, I believe, neither (except for maybe the REPL answer) does justice to Lisp. The Lisp family of languages have many features and architectural approaches. Even though these patterns differ from the normie ones, they still are this: patterns. In this post, I’m listing some of the general patterns that I’ve seen in mine and others’ code. From more generic (pun intended) to the local ones.
I’m deliberately ignoring OOP patterns. Lispy Object-Oriented systems (CLOS, EIEIO, GOOPS) might be strange and powerful. But most of the techniques from other OOP languages apply to them nonetheless. So let’s leave the space for more Lisp-specific patterns!
But to tip my hat to the OOP/Gang of Four people, I’ll start from one of their ideas: SECTION2(extension-points, Extension Points)
There’s an odd pattern among the ones Java programmers preach: Extension Point. Most likely no one understands what exactly that pattern means. It’s just “a part of your design open to later extension.” But if anything suits the “Extension Point” name, it’s Lisp code.
All the patterns below are a certain kind of extension point. Varying levels of complexity, assorted dialects, diverse features. But still, these are some of the things that make Lisp software flexible and loveable.
This top-level pattern is quite universal.
My friend recently raged about the overabundance of meaningless interfaces in Java and C#.
Golang is built around the idea of interfaces.
C++ boasts virtual classes.
Clojure allows to define
Common Lisp protocols usually come in the form of CD(defgeneric) declarations.
A perfect example of this approach is Shinmera’s work
like
cl-mixed with its heaps of back-ends.
Another example is JSON-parser-agnostic
NJSON
(shameless plug: I designed it.)
Here’s how a protocol definition looks there:
This type of architecture enforces a structure on the back-ends, while being adaptable to back-end swapping.
And this type of generalization also allows for...
If one can write a back-end and plug it into the software...
Then, well, anyone can do that.
Even the user.
Even if the software is quite intricate.
If it’s a mature enough Lisp, it likely has
This downstream-override approach is colloquially known as “monkey patching.”
It’s a basis for Aspect-Oriented Design, suggested by Gregor Kiczales.
The idiomatic example of that design is exactly what the OP of the Reddit post was asking about: logging.
In the the Aspect-Oriented-friendly application, logging can be added at any moment, just as an advice/override/CD(:around) function.
No need to put CD(console.log) all over the place, just make a special method and wrap your code into it!
Another example of this approach might be a home-brewed event system in Nyxt
(even though I left the team, I still love the code we’ve done together),
based on
Thanks to
u/arthurno1
for
mentioning this!
Hooks are variables/slots/objects effectively storing a list of functions (usually called “handlers”).
Once something happens, the functions stored inside the hook are called.
Emacs utilizes hooks as one of the main extension points, having hooks for:
Hooks may be said to be a subset/superset of the monkey-patched listeners.
But they are too conventional and useful to ignore.
I always
Not in Lisp.
Not in Common Lisp, in particular.
CL has this Condition System thingie that errors are part of.
Restarts are a notable feature of CL conditions that most other exception/error/condition systems lack.
Restarts are basically pieces of code you can invoke from the debugger to fix the error.
All the computation is effectively paused until the user chooses a restart to resume it.
Which can happen at an arbitrary moment or not even happen at all.
The point partially applies to other Lisps, because of REPL-driven development.
Even if something goes wrong, one’s just dropped into another level of debug loop.
While in there, one can redefine the broken functions and restart the computation.
Or ignore the error and
Ah, the dreaded global thread-unsafe state!
Even though most Lisps left the idea of dynamic scope behind, some still have it.
Common Lisp and Emacs Lisp both utilize dynamic variables... productively.
One particular example might be CL’s printer system.
There are
more than 15 globals
which influence printing.
Which sounds like a disaster, right?
Modifying global state just to change local logic sounds painful.
Luckily, the main use for dynamic variables is not setting them directly.
Binding them lexically with
Shameless plug again: my
Graven Image library.
(UPD Oct 2025: functionality mostly moved to Trivial Toplevel libs.)
There’s no custom printing in it, and all the APIs rely on the built-in functions like
I’m not even sure whether I should include that: it’s too low-level.
But dynamic variables are quite atomic (pun not intended) too, so here goes nothing.
Programming in Guile, I often miss CL’s function keyword.
There’s
There are several places keywords are vital:
Keywords might be passed real deep in the code paths.
That’s why CL’s
Update Apr 2024: I now understand and love Clojure map/sequence destructuring.
And Scheme
This is not the final list!
You can probably recall some other patterns better than my memory allows me to.
So feel free to reach out via
Reddit,
Hacker News comments,
or
email.
See also:
Clojure Design Patterns.
And my listing of Lisp documentation patterns on Nyxt website.
To use the air time productively: I’m searching for new projects/positions!
If your team looks for a CL/Clojure dev with a good knowledge of design patterns (duh), C, and JavaScript,
then we might be a good match!
Shoot me
an email
sometime 😉
One defines a generic function.
(defgeneric decode-from-stream (stream)
(:method (stream)
(signal 'decode-from-stream-not-implemented))
(:documentation "Decode JSON from STREAM.
Specialize on `stream' to make NJSON decode JSON."))
Monkey Patching (Emacs Lisp, CL)
Function overrides (any Lisp),
(defmethod (setf url) :around (value (buffer document-buffer))
(call-next-method)
(set-window-title))
Hooks (Emacs, CL, and others)
Emacs initialization, closing, saving and other editor-specific actions.
Fail Fast, Fail Often: Conditions (CL)
Server returned an error code?
Just
Shared State: Dynamic Variables (Emacs Lisp, CL)
(defmethod gimage:apropos* :around (string AMP()optional package external-only docs-too)
(let ((*print-case* :downcase)
(*print-level* 2)
(*print-lines* 2)
(*print-length* 10))
(call-next-method)))
Pythonique: Keywords and Destructuring (CL, Guile, Clojure)
(define* (jsc-class-make-constructor
class NUM():key (name %null-pointer) (callback #f)
(number-of-args (if callback (procedure-maximum-arity callback) 0)))
"Create a constructor for CLASS with CALLBACK called on object initialization..."
...)
to make functions with keywords in Guile>
Non-trivial function behavior:
Concluding