| link | Last. 20260309T210612550Z |
Unlike any other language - in the top 25 hot languages of 2025, at least - lisp is in its 30s. The language now is the only popular language that was standardised more than thirty years ago. Look at languages struggling to draft algebraic effects in the past year - but lisp has The Lisp Condition System (local restarts from external condition signal handling) in its standard, and has for more than 30 years (now, in 2026) of which lisp users are rightly proud. You can get this age number for any language by looking at wikipedia.org - it is the number labelled âstable releaseâ
- Python (programming language)
Stable release
3.14.3[3] Edit this on Wikidata / 3 February 2026; 33 days ago
- Fortran
Stable release
Fortran 2023 (ISO/IEC 1539:2023) / November 17, 2023; 2 years ago
- Java
Stable release
Java SE 25[2] Edit this on Wikidata / 16 September 2025; 5 months ago
- Common Lisp
standard document ANSI INCITS 226-1994 (S2018)
1994 (32 years ago) for ANSI Common Lisp
one of these things is not like the others. Do not be mystified by claims that the name âjavascriptâ was used back in the 90s: Javascript (wikipedia) used now is âJune 2024; 21 months agoâ.
It-not-being-a-baby adds a certain weight to common lisp the language. In other languages, programming libraries and programs are seen to be more stable than the language itself which is always trying to keep up with the Jonesâ. This is sort of true in common lisp- if you consider lispâs modern, lazy, optimizing infinite series macro package hails from 1978 and the US Department of Energyâs Maxima=Macsyma from 1982/1968
- Maxima (software)
Stable release
5.49.0[1] Edit this on Wikidata / 18 December 2025; 2 months ago
though we can see Maximaâs stable release is a baby, unlike the ansi common lisp standard.
We can clearly rely on writing ansi common lisp code as per the standard, portable between its many famous free and commercial compilers more than we can rely on any particular software subject to a software lifecycle, even an immortal jellyfish like Maxima.
Common lisp comes with a facility for modernising programs without changing them which is what the algebraic effects scientists are trying to copy into their languages this year. This is the ANSI common lisp condition system.
ls(defun %ls
(&optional (dir #p"./*.*") (rel #p"../")
&aux (rel-dir (probe-file rel)))
(dolist
(file (directory dir))
(print (enough-namestring file rel-dir))))
showing some of my blog and temporary files from emacs
CL-USER> (%ls)
"md/.#Section1.fragment.md"
"md/Section1.fragment.md"
"md/Section1.fragment.md~"
NIL
we can see this makes sense as a program for listing contents of directories.
This is great and all but I would actually like to argue that this program is not very future-proof. The implication is that I would change the program if I wanted to do something else. Like common lisp itself, I would like my programs to exhibit forward-compatibility, not just backwards-compatibility. Actually, I am just recently trying to fully get my head around this myself.
Let us try phrasing our program differently.
Note I added a
donesignal though we do not use it - similar to an:aftermethod-qualifier I guess.
(define-condition ls () ())
(define-condition want ()
((target :initarg :target :accessor target)))
(define-condition done ()
((result :initarg :result :accessor result)))
(define-condition want-ls (want ls) ())
(define-condition done-ls (done ls) ())
(defun %%ls
(target &rest other-keys &key &allow-other-keys)
(restart-case
(restart-case
(apply 'signal 'want-ls
:target target
other-keys)
(dirs
(c)
(setf (target c) (directory (target c)))
(signal c)))
(prints
(results)
(let ((res (map 'list 'print results)))
(signal 'done-ls
:result res)))))
But that is, as Kent tells us, a half-program. It signals a want-ls with our arguement, and has tiered restarts for retrieving directory contents, and printing a sequence.
(defun targets-dir (c)
(when (and (slot-boundp c 'target)
(typep (target c) '(or string pathname)))
(let ((r (find-restart 'dirs c)))
(invoke-restart r c))))
(defun prints-target (c)
(when (and (slot-boundp c 'target)
(listp (target c)))
(let ((r (find-restart 'prints c)))
(when r (invoke-restart r (target c))))))
As a first pass we specified two handlers that use the nested restarts in an obvious way. Note that we cannot restart uphill. And we bind those handlers at run-time:
(handler-bind
((want-ls #'targets-dir)
(want-ls #'prints-target))
(%%ls "*.*"))
e.g.:
CL-USER> (handler-bind
((want-ls #'targets-dir)
(want-ls #'prints-target))
(%%ls "*.*"))
#P"/home/screwlisp/screwlisps-kitten/fundamental/a-little-ansi-common-lisp/md/#Section1.fragment.md#"
#P"/home/screwlisp/screwlisps-kitten/fundamental/a-little-ansi-common-lisp/md/.#Section1.fragment.md"
#P"/home/screwlisp/screwlisps-kitten/fundamental/a-little-ansi-common-lisp/md/Section1.fragment.md"
#P"/home/screwlisp/screwlisps-kitten/fundamental/a-little-ansi-common-lisp/md/Section1.fragment.md~"
NIL
though I really want relative paths again. In our lisp condition system style, we do not have to and in fact should not touch our previous program code. Here the path kind of splits into three modestly different ways to extend the program without modifying it.
prints(defun list-rel-to (c &aux (rel #p"../"))
(when (and (slot-boundp c 'target)
(listp (target c))
(pathnamep (car (target c)))
(not (member :relative
(pathname-directory
(car (target c))))))
(invoke-restart
'prints
(mapcar (lambda (x) (enough-namestring x (probe-file rel)))
(target c)))))
(defun fun-again (c &aux (rel #p"../"))
(when (and (slot-boundp c 'target) ;; no work?
(listp (target c))
(pathnamep (car (target c)))
(not (member :relative
(pathname-directory
(car (target c))))))
(let ((res (mapcar (lambda (x)
(enough-namestring x (probe-file rel)))
(target c))))
(setf (target c) res)
(handler-bind ((want-ls #'prints-target))
(signal c)))))
(defun fun-retry (c &aux (rel #p"../"))
(when (and (slot-boundp c 'target)
(listp (target c))
(pathnamep (car (target c)))
(not (member :relative
(pathname-directory
(car (target c))))))
(invoke-restart
'retry
(mapcar (lambda (x)
(enough-namestring x (probe-file rel)))
(target c)))))
No matter which of these three is uncommented, the result is the same:
(handler-bind
((want-ls #'targets-dir)
(want-ls #'fun-retry)
(want-ls #'fun-again)
(want-ls #'list-rel-to)
(want-ls #'prints-target))
(restart-case
(%%ls "*.*")
(retry (target)
(%%ls target))))
Results:
CL-USER> (handler-bind
((want-ls #'targets-dir)
(want-ls #'fun-retry)
;;(want-ls #'fun-again)
;;(want-ls #'list-rel-to)
(want-ls #'prints-target))
(restart-case
(%%ls "*.*")
(retry (target)
(%%ls target))))
"md/#Section1.fragment.md#"
"md/.#Section1.fragment.md"
"md/Section1.fragment.md"
"md/Section1.fragment.md~"
NIL
CL-USER> (handler-bind
((want-ls #'targets-dir)
;;(want-ls #'fun-retry)
(want-ls #'fun-again)
;;(want-ls #'list-rel-to)
(want-ls #'prints-target))
(restart-case
(%%ls "*.*")
(retry (target)
(%%ls target))))
"md/#Section1.fragment.md#"
"md/.#Section1.fragment.md"
"md/Section1.fragment.md"
"md/Section1.fragment.md~"
NIL
CL-USER> (handler-bind
((want-ls #'targets-dir)
;;(want-ls #'fun-retry)
;;(want-ls #'fun-again)
(want-ls #'list-rel-to)
(want-ls #'prints-target))
(restart-case
(%%ls "*.*")
(retry (target)
(%%ls target))))
"md/#Section1.fragment.md#"
"md/.#Section1.fragment.md"
"md/Section1.fragment.md"
"md/Section1.fragment.md~"
NIL
CL-USER> (handler-bind
((want-ls #'targets-dir)
(want-ls #'fun-retry)
(want-ls #'fun-again)
(want-ls #'list-rel-to)
(want-ls #'prints-target))
(restart-case
(%%ls "*.*")
(retry (target)
(%%ls target))))
"md/#Section1.fragment.md#"
"md/.#Section1.fragment.md"
"md/Section1.fragment.md"
"md/Section1.fragment.md~"
NIL
Note we should have used find-restart to check if the restarts were available on the condition before invoking them, but in this example we knew they were there.
In the 70s, Sandewall coined the delete problem about reasoning structures like this. When your program works but uses non-trivial condition handling, meaning here that more than one signal can happen - removing a condition or restart might drastically change the programâs behaviour (or not).
To put it pithily, emergent behaviours are chaotic. While we are purely good-old-fashioned-ai (list processing!) here, the delete problem has far-ranging implications about censorship in todayâs large language models - doctoring the training data to remove something bad that is real could be expected to derange the model.
We saw that the ansi common lisp standard - the only widely used programming language whose current definition is in its 30s (in 2026) - defines a condition system which is coming into vogue in other languages today under the name algebraic effects.
In particular, we saw that run-time handler-bind handlers are able to locally handle and invoke-restart inside a pre-existingly programmed program, which lets us customize and extend programs without changing them. This is of fundamental importance, because repeatedly changing the source of a widely used program is known in software engineering to be a disaster. We would like the program to stay intact and reliable while we choose how we interact with that program in our own context at runtime.
This is an example of John Masheyâs (unix/bell labs) Small-is-beautiful design principle. https://www.usenix.org/legacy/events/bsdcon/mashey_small/
Sandewallâs delete problem shows that changing a program has chaotic effects. So we would like to extend and customize what our program does at run-time without changing the programâs definition for everyone, everything and everywhere else. This is a fundamental programming style.
check-type macro is a standard example of condition handling.