named-closure

2024-10-12

Named closures

Upstream URL

github.com/BlueFlo0d/named-closure

Author

Qiantan Hong <qhong@alum.mit.edu>

Maintainer

Qiantan Hong <qhong@alum.mit.edu>

License

GPLv3+
README
NAMED-CLOSURE - introspectable and redefinable closures

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:

  1. 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.
  2. 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.
  3. 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:

  1. Concise syntax.
  2. 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 in BODY, has readable printsyntax, and if nclo with the same NAME 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
          (defnclo something (lambda-list-1...)
              (lambda-list-2...)
            body...)
    
    is similar to
          (defun make-something (lambda-list-1...)
            (lambda (lambda-list-2...)
              body...))
    
    except that make-something now returns a funcallable object withslots corresponding to variables declared in lambda-list-1, hasreadable print syntax, and re-evaluating the defnclo 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!!!

Saying nclo is similar to lambda is a lie. Currently, ncloeffectively 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.

Dependencies (4)

  • alexandria
  • closer-mop
  • hu.dwim.util
  • hu.dwim.walker

Dependents (0)

    • GitHub
    • Quicklisp