Dynamic binding without special in Common Lisp

:: lisp, programming

In Common Lisp, dynamic bindings and lexical bindings live in the same namespace. They don’t have to.

Common Lisp has two sorts of bindings for variables: lexical binding and dynamic binding. Lexical binding has lexical scope — the binding is available where it is visible in source code — and indefinite extent — the binding is available as long as any code might reference it. Dynamic binding has indefinite scope — the binding is available to any code which runs between when the binding is established and when control leaves the form which established it — and dynamic extent — the binding ceases to exist when control leaves the binding form.

These are really two very different things. However CL places both of these kinds of bindings into the same namespace, relying on special declarations and proclamations to tell the system which sort of binding to create and reference for a given name.

That doesn’t have to be the case: it’s possible in CL to completely isolate these two namespaces from each other. This means you could write code where all variable references were to lexical bindings and where dynamic bindings were created and referenced by a completely different set of operators. Here is an example of that. Following practice in some old Lisps I will call this ‘fluid’ binding. I will also use / to delimit the names of fluid variables simply to distinguish them from normal variables.

(defun inner (varname value)
  (setf (fluid-value varname) value))

(defun outer (varname value)
  (call/fluid-bindings
   (lambda ()
     (values
      (fluid-value varname)
      (progn
        (inner varname (1+ value))
        (fluid-value varname))))
   (list varname)
   (list value)))

And now

> (outer '/v/ 1)
1
2

Here are a set of operators for dealing with these fluid variables:

fluid-value accesses the value of a fluid variable.

fluid-boundp tells you if a name is bound as a fluid variable.

call/fluid-bindings calls a function with one or more fluid variables bound.

define-fluid (not used above) defines a global value for a fluid variable.

Well, of course you can do something like this using an explicit binding stack and a single special variable to hang it from. But that’s not how this works: these ‘fluid variables’ are just CL’s dynamic variables:

(defun call/print-base (f base)
  (call/fluid-bindings  f '(*print-base*) (list base)))
> (call/print-base
   (lambda ()
     *print-base*)
   2)
2

So how does this work? Well fluid-value and fluid-boundp are obvious:

(defun fluid-value (s)
  (symbol-value s))

(defun (setf fluid-value) (n s)
  (setf (symbol-value s) n))

(defun fluid-boundp (s)
  (boundp s))

And the trick now is that CL gives you enough mechanism to bind named dynamic variables yourself, that mechanism being progv, which

[…] allows binding one or more dynamic variables whose names may be determined at run time […]

So now call/fluid-bindings just uses progv:

(defun call/fluid-bindings (f fluids values)
  (progv fluids values (funcall f)))

And finally define-fluid looks like this:

(defmacro define-fluid (var &optional (value nil)
                            (doc nil docp))
  `(progn
     (setf (fluid-value ',var) ,value)
     ,@(if docp
           `((setf (documentation ',var 'variable) ',doc))
         '())
     ',var))

The interesting thing here is that there are no special declarations or proclamations: you can create and bind new fluid variables without any recourse to special at all, in a way which is completely compatible with the existing dynamic variables, because fluid variables are dynamic variables.

So one way of thinking about special is that it is a declaration that says ‘for this variable name, access the namespace of dynamic bindings rather than lexical bindings’. This is not really what special was of course in Lisps before CL — it was historically closer to an instruction to use the interpreter’s variable binding mechanism in compiled code — but you can think of it this way in CL, where the interpreter and compiler do not have separate binding rules.

And, of course, using something like the above, you could write code in CL where all variable bindings were lexical and dynamic variables lived entirely in their own namespace. For instance this works fine:

(defun f ()
  (let ((x 2))
    (call/fluid-bindings
     (lambda ()
       (values x (fluid-value 'x)))
     '(x) '(3))))
> (f)
2
3

The reference to x as a variable refers to its lexical binding, while (fluid-value 'x) refers to its dynamic binding.

Whether writing code like that would be useful I am not sure: I think that the *-convention for dynamic variables is perfectly fine in fact. But it is perhaps interesting to see that you can think of dynamic bindings in CL this way.