0

Is there any standard way in Common Lisp (+common libraries today) how to put default settings for not-yet-loaded libraries/systems (that are from public sources and cannot be reasonably modified) in the config file such as .sbclrc?

For example, I want to have set

 (setf quickproject:*template-directory* "~/projects/template/"
       quickproject:*author* "my name <etcetc>")

when Quickproject is loaded. (Obviously, there is an additional complication with the relevant package not being defined at that point of time, but this I can handle.)

Something along the lines of emacs's (with)-eval-after-load.

I am interested primarily in sbcl and quicklisp/asdf libraries, but implementation independent solution is preferred.

I tried to find in existing libraries and out of the box functionalities of SBCL require and of asdf, without success.

I considered and rejected abusing sbcl's extensions on trace.

The best I was able to do so far is define method for asdf:operate, something like (for illustration, not really used)

(defvar *after-system-hooks* (make-hash-table :test 'equal))

(defmethod asdf:operate :after ((op asdf:load-op) (component asdf:system) &key)
  (let* ((name (asdf:component-name component)))
    (dolist (hook (gethash name *after-system-hooks*))
      (funcall hook))
    (setf (gethash name *after-system-hooks*) nil)))

(defun intern-uninterned (code package)
  (typecase code
    (cons (cons (intern-uninterned (car code) package)
                (intern-uninterned (cdr code) package)))
    (symbol (if (symbol-package code) code (intern (symbol-name code) package)))
    (t code)))

(defmacro with-eval-after-load ((name package) &body body)
  `(let ((fn (lambda () (eval (intern-uninterned '(progn ,@body) ',package)))))
     (if (member ,name (asdf:already-loaded-systems) :test 'equal)
         (funcall fn)
         (push fn (gethash ,name *after-system-hooks*)))))

and then write something like

(with-eval-after-load ("quickproject" :quickproject)
  (setf
   #:*template-directory*  "~/projects/template/"
   #:*author*   "My Name <etcetc>"))

However, this is overwriting method on standard component dispatched on standard classes, so it does not seem safe and future-proof. And I do not see how to define new classes to specialize in such a way that it would work with existing systems and tooling.

(And it has eval, but it does not bother me much here)

Ken White
  • 123,280
  • 14
  • 225
  • 444
Tomas Zellerin
  • 366
  • 1
  • 7

1 Answers1

1

You are on the right track with asdf:operate. But instead of specializing on asdf:system in general, use EQL Specializer:

(defmethod asdf:operate :after ((op asdf:load-op) (component (eql (asdf:find-system "quickproject"))) &key)
           ... )

This method only gets called after loading Quickproject.

I would also get rid of extra abstractions and just do want I want to do in the method itself. There is uiop:symbol-call to call a function which isn't loaded yet. But I wouldn't mind using eval with a fixed string.

To use this method (and similar custom forms) from multiple implementations, just put this in a shared-init.lisp file and then load this file in each implementation's init file.

As for common libraries for settings and configs for your own code, I have used https://github.com/Shinmera/ubiquitous before, which worked without any issues.

Nisar Ahmad
  • 302
  • 1
  • 4
  • This might be better in some ways, but I think it does not solve the concern - the methods on `(eql )` are often defined by asdf packagers, so risk of conflict remains. True, most methods are defined on `perform` (that might be better in this implementation anyway, to prevent duplicate executions) and only one in my installed packages on `operate`. – Tomas Zellerin Dec 22 '22 at 08:21