named-closure
2024-10-12
Named closures
Upstream URL
Author
Maintainer
License
1Why?
Hot take: closures in Common Lisp and most lisps are broken forlong-running images. (Maybe more so in non-lisps, but who cares.)Comparing to function symbols, closures are nearly unusable in the following aspects:
- They're hard to introspect, both from a user aspect and (evenmore so) programmatically. SBCL for example allows you toretrieve values in the flat closure, but do not save variableinformation.
- As a consequence, they in general don't have readable printsyntax, and it might be impossible to write one. Function symbolson the other hand can be print and then read to get the exactlysame object.
- They're near impossible to redefine. For a function symbol,setting its function cell causes all call site to consistentlycall the new definition. This is impossible for closures.
Why redefining closure? It allows you to fix buggy code for closure (nobody always write correct code the first time!). Without redefinability you'll have closure with wrong code floating around in the image that is almost impossible to fix, unless you remember all the location the closure is used and fix it manually -- I find it much less pleasant and isn't always possible.
Closures are still useful because:
- Concise syntax.
- They're the lingua franca for a whole bunch of "functionalprograms", which expect objects that are funcallable.
2What
NAMED-CLOSURE
provides DEFNCLO
and NCLO
.(nclo NAME LAMBDA-LIST . BODY)
is similar to ~(lambdaLAMBDA-LIST . BODY)~, but returns a funcallable object with slotscorresponding to free variable inBODY
, has readable printsyntax, and ifnclo
with the sameNAME
is encountered (forexample, if re-evaluated from REPL), the function definition ofall such funcallable objects is updated. Closed variables with thesame names are carried over across update.defnclo
is similar to(defnclo something (lambda-list-1...) (lambda-list-2...) body...)
except that(defun make-something (lambda-list-1...) (lambda (lambda-list-2...) body...))
make-something
now returns a funcallable object withslots corresponding to variables declared inlambda-list-1
, hasreadable print syntax, and re-evaluating thedefnclo
updates thefunction definition of all such funcallable objects. Closedvariables with the same names are carried over across update.
Note: newly introduced variables are unbound for updated old
closures! This will likely cause an unbound-slot
condition when
such closure is called. You're free to use the store-value
restart
your implementation (usually) provides to fix up the closure if
possible. There isn't anything more we can help :/
3!!!Caveat!!!
Sayingnclo
is similar to lambda
is a lie. Currently, nclo
effectively copies the captured environment instead of directlylink to it. See https://github.com/BlueFlo0d/named-closure/issues/1to understand the important behavioral difference between nclo
and lambda
.3.1Excuses
It is possible to simulate the "correct" environment sharing behavior
identical to lambda
. I'm currently not doing it because
- It complicates introspection and proper readable printing
- It nukes upgradability. While it's not unreasonable to ask userto fix up old closures using
store-value
, it sounds prettyimpratical to expect user to fix the sharing structure betweendifferent closures. - Closures are convenient. But remember we have real objects!If you need to rely on multiple closures sharing one environments,maybe it's better to just use CLOS.
4Example
(use-package :named-closure)
(defun make-inc-1 (x) (nclo inc (y) (setf x (+ x y))))
(defparameter *test-instance* (make-inc-1 5))
*test-instance* ; => #.(MAKE-INC :X 5)
(funcall *test-instance* 6) ; => 11
(funcall *test-instance* 6) ; => 17
(defun make-inc-1 (x) (nclo inc (y) (setf x (- x y)))) ; changed our mind!!!
(funcall *test-instance* 6) ; => 11
(funcall *test-instance* 6) ; => 5
p.s. I will probably ensure NAME-CLOSURE
only ever exports obscure
names, so it should be quite safe to use-package
it!
5How
Under the hood,defnclo
defines a funcallable class namedsomething
, which in turn indirect calls through itsclass-allocated ~'named-closure::code~ slot so that it isredefinable.nclo
is implemented by walking BODY
and collecting its free
variables, then calling defnclo
with the free variable list (with
&key
prepended) passed as LAMBDA-LIST-1
.
Note: Because free variables are converted to keyword argument,
their symbol-name
must be distinct. Is this good enough?
There's one subtlety involved with nclo
: nclo
usually appears as
a non-top-level form, but it needs to ensure creating a top-level
function definition for NAME
in the runtime environment. We do this
by abusing load-time-value
.