The first thing to realise that there's no good way to have a dynamically-scoped slot in an object (unless the implementation has some deep magic to support this): the only approach that will work is to use, essentially, explicit shallow-binding. Something like this macro, for instance (this has no error checking at all: I just typed it in):
(defmacro with-horrible-shallow-bound-slots ((&rest slotds) object &body forms)
(let ((ovar (make-symbol "OBJECT"))
(slot-vars (mapcar (lambda (slotd)
(make-symbol (symbol-name (first slotd))))
slotds)))
`(let ((,ovar ,object))
(let ,(mapcar (lambda (v slotd)
`(,v (,(first slotd) ,ovar)))
slot-vars slotds)
(unwind-protect
(progn
(setf ,@(mapcan (lambda (slotd)
`((,(first slotd) ,ovar) ,(second slotd)))
slotds))
,@forms)
(setf ,@(mapcan (lambda (slotd slot-var)
`((,(first slotd) ,ovar) ,slot-var))
slotds slot-vars)))))))
And now if we have some structure:
(defstruct foo
(x 0))
Then
(with-horrible-shallow-bound-slots ((foo-x 1)) foo
(print (foo-x foo)))
expands to
(let ((#:object foo))
(let ((#:foo-x (foo-x #:object)))
(unwind-protect
(progn (setf (foo-x #:object) 1) (print (foo-x foo)))
(setf (foo-x #:object) #:foo-x))))
where all the gensyms with the same name are in fact the same. And so:
> (let ((foo (make-foo)))
(with-horrible-shallow-bound-slots ((foo-x 1)) foo
(print (foo-x foo)))
(print (foo-x foo))
(values))
1
0
But this is a terrible approach because shallow binding is terrible in the presence of multiple threads: any other thread that wants to look at foo
's slots will also see the temporary value. So this is just horrid.
A good approach is then to realise that while you can't safely dynamically-bind a slot in an object, you can dynamically bind a value which that slot indexes by using a secret special variable to hold a stack of bindings. In this approach the values of slots do not change, but the values they index do, and can do so safely in the presence of multiple threads.
A way of doing this this is Tim Bradshaw's fluids
toy. The way this works is that you define the value of a slot to be a fluid, and then you can bind that fluid's value, which binding has dynamic scope.
(defstruct foo
(slot (make-fluid)))
(defun outer (v)
(let ((it (make-foo)))
(setf (fluid-value (foo-slot it) t) v) ;set global value
(values (fluid-let (((foo-slot it) (1+ (fluid-value (foo-slot it)))))
(inner it))
(fluid-value (foo-slot it)))))
(defun inner (thing)
(fluid-value (foo-slot thing)))
This often works better with CLOS objects because of the additional flexibility in things like naming and what you expose (you almost never want to be able to assign to a slot whose value is a fluid, for instance: you want to assign the value of the fluid).
The system uses a special variable behind the scenes to implement deep binding for fluids, so will work properly with threads (ie distinct threads can have different bindings for a fluid) assuming the implementation treats special variables sensibly (which I'm sure all multithreaded implementations do).