| link | Last. 20260201T213438845Z |
I owe you this article after last week’s Lispy Gopher Climate Tuesday Night In The Americas where Kent Pitman pointed out two improvements to my note on common lisp condition signalling, handling and restarting. Lisp condition handling restarts are local continuations, i.e. a dynamically computed available restart is done from the location the condition was signalled rather than from a lexically scoped preordained point.
Until now, my original example had been like this:
(define-condition foo () ())
(define-condition bar () ())
(defun has-a-bar->buz (c)
(declare (ignore c))
(restart-case
(handler-bind
((bar #'(lambda (c) (invoke-restart (find-restart 'buz) c))))
(restart-case
(signal 'bar)
(buz (c) (declare (ignore c))
(print "inner buz reached")
(print (compute-restarts))
(let ((r (find-restart 'buz)))
(print r)
(invoke-restart r c)))))))
(restart-case
(handler-bind
((foo #'has-a-bar->buz)
(foo #'(lambda (c)
(invoke-restart (find-restart 'baz) c)))
(bar #'(lambda (c)
(invoke-restart (find-restart 'buz) c))))
(signal 'foo))
(baz (c) (declare (ignore c)) (print "baz reached") nil)
(buz (c) (declare (ignore c)) (print "buz reached") nil))
Kent’s two improvements were to generally use the condition optional argument when using find-restart:
(find-restart 'baz c)
for the condition c. This is important because while handling a condition, a second condition could be signalled. Then, the two conditions can and probably will have different computed restarts available to them. So when finding a local restart by name, in general do specify the condition you want to do the restart for. find-restart will compute and return the restart (again, local continuation) of that name, specifically the one available to the optional condition argument if it is specified.
Which brings us to Kent’s second point, that find-restart returns nil if no applicable restart is found, and I was calling invoke-restart on a value that might be a restart or might be nil and invoke-restart only takes restarts.
CL-USER> (invoke-restart nil)
The value
NIL
is not of type
(OR (AND SYMBOL (NOT NULL)) RESTART)
when binding RESTART
[Condition of type TYPE-ERROR]
Kent noted it is possible to intend that this common lisp type-error is signalled when find-restart returns NIL (fails to find an applicable restart) but more commonly you want the condition handler to decline (return without restarting), allowing the next handler to handle (or decline) the condition.
In other words, normally a handler should decline a condition (errors are one example of conditions) that it cannot handle (provide a local restart continuation from) allowing the next handler to accept or decline a condition. Only in particular scenarios should the unavailability of a restart to one handler signal a type error condition of its own.
Rewriting,
(define-condition foo () ())
(define-condition bar () ())
(defun has-a-bar->buz (c)
(declare (ignore c))
(restart-case
(handler-bind
((bar #'(lambda (c)
(let ((r (find-restart 'buz c)))
(when r
(invoke-restart r c))))))
(restart-case
(signal 'bar)
(buz (c)
(print "inner buz reached")
(print (compute-restarts))
(let ((r (find-restart 'buz c)))
(print r)
(when r
(invoke-restart r c))))))))
(restart-case
(handler-bind
((foo #'has-a-bar->buz)
(foo #'(lambda (c)
(let ((r (find-restart 'baz c)))
(when r
(invoke-restart r c)))))
(bar #'(lambda (c)
(let ((r (find-restart 'buz c)))
(when r
(invoke-restart r c))))))
(signal 'foo))
(baz (c) (declare (ignore c)) (print "baz reached") nil)
(buz (c) (declare (ignore c)) (print "buz reached") nil))
which works the same in this case, but now the find-restart is more specific which is better style, and the handlers would decline (the general case) rather than signal an error condition if they did not have a restart. I am also utilising the conditions I am passing along with invoke-restart rather than just ignoring them in more cases. (Also, invoke-restart actually passes arbitrary additional arguments to the restart being invoked, not just conditions specifically).
CL-USER> (define-condition foo () ())
(define-condition bar () ())
(defun has-a-bar->buz (c)
(declare (ignore c))
(restart-case
(handler-bind
((bar #'(lambda (c)
(let ((r (find-restart 'buz c)))
(when r
(invoke-restart r c))))))
(restart-case
(signal 'bar)
(buz (c)
(print "inner buz reached")
(print (compute-restarts))
(let ((r (find-restart 'buz c)))
(print r)
(when r
(invoke-restart r c))))))))
HAS-A-BAR->BUZ
CL-USER> (restart-case
(handler-bind
((foo #'has-a-bar->buz)
(foo #'(lambda (c)
(let ((r (find-restart 'baz c)))
(when r
(invoke-restart r c)))))
(bar #'(lambda (c)
(let ((r (find-restart 'buz c)))
(when r
(invoke-restart r c))))))
(signal 'foo))
(baz (c) (declare (ignore c)) (print "baz reached") nil)
(buz (c) (declare (ignore c)) (print "buz reached") nil))
"inner buz reached"
(#<RESTART BAZ {7F8914836A23}> #<RESTART BUZ {7F89148369E3}>
#<RESTART SWANK::RETRY {7F8914836D53}> #<RESTART ABORT {7F89148370C3}>
#<RESTART ABORT {7F8914837AF3}>)
#<RESTART BUZ {7F89148369E3}>
"buz reached"
NIL
A more general note was that because available local restarts from signalled conditions are computed and not lexical, it is important style not to use restarts and condition handling as though they had been lexical which would be misleading.
This lisp thought-leadership is a challenge because basically everyone else (such as python or java) just has lexically scoped error handling rather than lisp’s dynamically computed local restart continuations from signalled conditions (errors are a subgroup of conditions).
We should embrace lisp’s condition system
to think previously impossible thoughts
to repeat Dijkstra’s phrase on lisp.