Sunday, August 23, 2009

Global variables done right

If you've read my Idiot's Guide to Special Variables you will already know that I am not a big fan of the design of Common Lisp's global variables system. There are (at least) three problems with CL's design:

1. There are no global lexicals.

2. DEFCONSTANT doesn't actually define a constant, it defines a global variable with the less-than-useful property that the consequences of attempting to change its value are undefined. Thus, a conforming implementation of Common Lisp could expand DEFCONSTANT as DEFVAR. (I am, frankly, at a loss to understand why DEFCONSTANT was even included in the language. As far as I can tell there is nothing that you can do in portable CL with DEFCONSTANT that you could not do just as well with DEFINE-SYMBOL-MACRO.)

3. There is no way to declare a global variable without also making the name of the variable pervasively special. In other words, once you've created a global variable named X it is no longer possible to write new code that creates lexical bindings for X. It is, of course, still possible for code evaluated before X was declared special to create lexical bindings for X. IMHO this is just insane. The only reason for this design is for backwards compatibility with code written for dynamically scoped dialects of Lisp. Well, guess what, folks. It's 2009. There are no more dynamically scoped dialects of Lisp. (Well, there's eLisp, but it lives safely sequestered in its own world and can be safely ignored by rational people.) And if you're still unconvinced that Common Lisp's pervasive special declarations are a bad design, consider the rule that all global variables should have named that are bookended by asterisks. Any time you need to impose a rule like that on your programmers, your language design is broken.

Fortunately, the situation is not all that hard to remedy. All that is needed is to implement the hypothetical L-LET and D-LET constructs from the Idiot's Guide, and to provide a way to declare global variables in a way that doesn't make them globally special.

Ladies and Gentlegeeks, I give you Global Variables Done Right:




(defun get-dynamic-cell (symbol)
(or (get symbol 'dynamic-cell) (setf (get symbol 'dynamic-cell) (copy-symbol symbol))))

(defun dynamic-value (symbol) (symbol-value symbol))

(defmacro defv (var val)
"Defines VAR to be a global dynamic variable with initial value VAL"
`(progn
(setf (symbol-value ',(get-dynamic-cell var)) ,val)
(define-symbol-macro ,var (dynamic-value ',(get-dynamic-cell var)))))

(defmacro dval (var)
"Returns the current dynamic binding of VAR, even if there is a lexical binding in scope"
`(symbol-value ',(get-dynamic-cell var)))

(defmacro dlet (bindings &body body)
"Unconditionally create new dynamic bindings"
(if (atom bindings) (setf bindings `((,bindings ,(pop body)))))
(let* ((vars (mapcar 'first bindings))
(dvars (mapcar 'get-dynamic-cell vars))
(vals (mapcar 'second bindings)))
(dolist (v vars)
(let ((e (macroexpand v)))
(if (or (atom e) (not (eq (car e) 'dynamic-value)))
(error "~A is not a dynamic variable" v))
(if (eq (car e) 'non-settable-value) (error "~A is immutable" v))))
`(let ,(mapcar 'list dvars vals) (declare (special ,@dvars)) ,@body)))

(defun get-lexical-cell (sym)
(or (get sym 'lexical-cell) (setf (get sym 'lexical-cell) (copy-symbol sym))))

(defun non-settable-value (s) (symbol-value s))
(defun (setf non-settable-value) (val var)
(declare (ignore val))
(error "~A is immutable" var))

(defmacro defc (var val &optional force-rebind)
"Immutably binds VAR to VAL. If FORCE-REBIND is T then VAR is forcibly rebound."
(let ((cell (get-lexical-cell var)))
`(progn
,(if force-rebind
`(setf (symbol-value ',cell) ,val)
`(unless (boundp ',cell) (setf (symbol-value ',cell) ,val)))
(define-symbol-macro ,var (non-settable-value ',cell)))))

(defmacro deflexical (var val)
"Defines VAR to be a global lexical variable"
(let ((cell (get-lexical-cell var)))
`(progn
(setf (symbol-value ',cell) ,val)
(define-symbol-macro ,var (symbol-value ',cell)))))

(defmacro lval (var)
"Unconditionally returns the global lexical binding of VAR"
`(symbol-value ',(get-lexical-cell var)))




The best way to show what this code does is with an example:




? (defc constant1 "Constant value")
CONSTANT1
? (setf constant1 "Can't change a constant")
> Error: CONSTANT1 is immutable
> While executing: (SETF NON-SETTABLE-VALUE), in process Listener(6).
> Type cmd-. to abort, cmd-\ for a list of available restarts.
> Type :? for other options.
1 >
? (defc constant1 "Can't change a constant value, take 2")
CONSTANT1
? constant1
"Constant value"
? (defc constant1 "Can rebind a constant by specifying FORCE-REBIND" t)
CONSTANT1
? constant1
"Can rebind a constant by specifying FORCE-REBIND"

? (defv v1 "Global dynamic variable")
V1
? (deflexical l1 "Global lexical variable")
L1
? (defun test1 () (list v1 l1))
TEST1
? (test1)
("Global dynamic variable" "Global lexical variable")
? (let ((v1 1) (l1 1)) (list v1 l1 (test1)))
(1 1 ("Global dynamic variable" "Global lexical variable"))
? (dlet ((v1 "Dynamic binding 1") (l1 "Dynamic binding 2")) (list v1 l1 (test1)))
("Dynamic binding 1" "Dynamic binding 2" ("Dynamic binding 1" "Global lexical variable"))
? (let ((v1 1)) (list v1 (dval v1)))
(1 "Global dynamic variable")
? (let ((l1 1)) (list l1 (lval l1)))
(1 "Global lexical variable")

; Watch this trick!
? (deflexical v1 "New global lexical binding for what was a dynamic variable")
V1
? (defun foo () v1)
FOO
? (let ((v1 1)) (list v1 (dval v1) (lval v1) (foo)))
(1 "Global dynamic variable" "New global lexical binding for what was a dynamic variable" "New global lexical binding for what was a dynamic variable")
? (dlet ((v1 1)) v1)
> Error: V1 is not a dynamic variable




Things to note:

1. The design is completely orthogonal. In fact, a single variable can have a local lexical binding, a global lexical binding, and a dynamic binding all at the same time, and all of which are accessible in a single scope. No more pervasive special declarations. (In fact, no more special declarations at all. They are replaced with the DLET macro.)

2. Constants are enforced to be immutable unless this is explicitly overridden by specifying FORCE-REBIND to be true.

3. Because the design is orthogonal, it is actually a design choice whether dynamically binding a global lexical should be an error. There is no reason why this couldn't be allowed to proceed, to create a dynamic binding that could then be accessed (only) via the DVAL macro. But I decided that although it's possible to have both global lexical and dynamic bindings for the same variable at the same time, it's probably not a good idea.

7 comments:

  1. Neat.

    Why do you want to allow constants to be rebound? Just for ease of debugging? I imagined the point of a constant was to allow the compiler to partially evaluate code that uses it. What requirements are you suggesting on an implementation that has done so, if the constant gets rebound?

    I suppose the easiest thing would be: old uses of the constant are allowed to use the old value, but any new use will see the new value. (The alternative, of requiring the implementation to remember every place the constant got used in the past, seems to take away a lot of the benefit of having constants in the first place.)

    ReplyDelete
  2. > Neat.

    Thanks! :-)

    > Why do you want to allow constants to be rebound? Just for ease of debugging?

    Yep. I'm optimizing for ease of learning and development, not execution speed. If you want to optimize for execution speed you still have DEFCONSTANT and DEFINE-SYMBOL-MACRO.

    > I suppose the easiest thing would be: old uses of the constant are allowed to use the old value, but any new use will see the new value.

    Actually the way it works now, old uses see the new value:

    ? (defc x 1)
    X
    ? (defun foo () x)
    FOO
    ? (foo)
    1
    ? (defc x 2 t)
    X
    ? (foo)
    2

    This is because "constants" defined by DEFC are not really constants, they're immutable variables. References to "constants" defined by DEFC still do an indirection:

    ? (macroexpand 'x)
    (NON-SETTABLE-VALUE '#:X)

    You can make them into "real" constants (in the sense of DEFCONSTANT) by redefining NON-SETTABLE-VALUE as a macro. For the moment this is left as an exercise ;-)

    ReplyDelete
  3. First off -- sounds like a good idea. defconstant's weird semantics have bugged me for ages. Any chance of you pushing it into CLTL3 or something like that, or otherwise getting it into a state where we can handily use it? (I.e. a package?)

    My question is about the use of copy-symbol? Why do you do that?

    ReplyDelete
  4. > Any chance of you pushing it into CLTL3 or something like that, or otherwise getting it into a state where we can handily use it? (I.e. a package?)

    I'm pretty sure there isn't going to be a CLTL3, but I am planning on rolling this code into my lexicons package and making that available. I actually have a grand plan of creating and maintaining a comprehensive library of useful utilities, but that's not going to happen for a while.

    > My question is about the use of copy-symbol? Why do you do that?

    I need some place to serve as a storage location for lexical and dynamic values. There are various ways to do that. You can use a cons cell, but that wastes memory, which offends my sensibilities. You can use a struct with one slot. That might be the Right Answer. I didn't give it a whole lot of thought. Using a symbol has the advantage that you can abbreviate (symbol-value 'X) as simply X, so that simplifies the code. Using copy-symbol instead of gensym or make-symbol is a more or less arbitrary choice. It gives you a symbol with the same name, which makes it in some sense easy to figure out what is going on. But one could argue that the Right Name for a symbol that stores, say, the lexical value of X is X-lexical-value or something like that.

    ReplyDelete
  5. > I'm pretty sure there isn't going to be a CLTL3

    Do you know about this new effort?
    http://ilc2009.scheming.org/node/48
    the mailing list is here:
    http://common-lisp.net/pipermail/cltl3-devel/

    ReplyDelete
  6. > Do you know about this new effort?

    Nope. Thanks for the pointer.

    ReplyDelete