The Magic of Dynamic Scope
Last edited Mon, 1 Aug 2005
Most programmers have been taught from time that they are very small children that global variables are evil. "Proper" programming wisdom teaches us to never ever use a global, no matter how convenient it may be. Now too be fair most of the people dispensing this advice are coming from a very odd point of view: they think you are going to write your application in C or Java. It may seem very odd that anyone would choose to write a program in such a language if they don't absolutely have too, but unfortunately it remains standard practice to use these languages for just about everything, and if you look at globals from a C programmers point of view, they are a total disaster. In C, globals kill reentrancy. In C globals are truly and irrevocably global, which means two instances of your program in the same address space have to share that variable, which usually means they simply cannot coexist. Global-ridden libraries cannot be used by more than one thread. They cannot be used in signal handlers, and it is very inconvenient (and probably impossible without the source) to use multiple instances of them even in a totally synchronous program. So where the heck do you put all of that global context that your program needs? Where do you put your output stream, and you xlib handle? Where do you put the --debug flag? A C programmer's answer to these questions is typical of the drudgery the poor wretch is always being forced to endure. He would say "You take all the things you want to be globals and stuff them in a structure called 'context' or something, and then pass the tiresome thing around between every function in your program".
Perhaps you don't mind passing context handles around like you have nothing better to do. Perhaps you don't mind casting every little object you pull out of a java.lang.Vector either. Maybe you don't even mind boxing your own ints so javac lets you pass them by reference. If you are such a person then by all means stick to C or Java or Fortran or an abacus or whatever you use. But if you are the sort of person that objects to doing the compilers job for it, then I am here to tell you that Yes, there is a better way. The answer to this problem of where to store all your global data if you can't use global variables is called dynamic scope. The idea is you simply put that data in global variables to start with, but allow those bindings to be locally shadowed by new ones. For example:
;; define a global variable *x* with initial value 'foo
(defvar *x* 'foo)
;; define a function that prints *x*
(defun printx ()
(print *x*))
;; this will print "foo"
(printx)
;; this will print "bar" and "baz"
(let ((*x* 'bar)) ;; create a new binding of the symbol *x* that is
;; visible within this let form
(printx)
(setq *x* 'baz) ;; set *x* to 'baz
(printx))
;; this will print "foo" again
(printx)
It might look like let is simply saving and restoring the value of *x* but in reality that is not what it's doing at all. Let is creating a whole new variable called *x*. Any code within the let, or any code called by any code within the let will have access to this new *x* instead of the global one. The distinction between creating a new variable and saving/restoring an existing one is that the new variable is unaffected by other threads, and the old variable cannot be fouled up by non-local exits (i.e. exceptions). The language guarantees that the only code that will see the new *x* is code that is truly inside the dynamic scope of the let. Dynamic scope is even perfectly compatible with continuations. It can be implemented in scheme via dynamic-wind
Dynamic scope is good for more than just storing output streams and debug flags. Together with non local exits, dynamic scope is the basis of common lisp's mind-blowingly awesome condition system. A condition is like an exception, except:
- They need not be exceptional, i.e. the default response to someone throwing a condition is allowed to be "do nothing"
- Code that signals conditions can provide restarts as well. A restart is a subroutine that can be called to recover from the condition and continue with the computation. For instance a lisp IMAP library might throw "invalid password" condition, but instead of simply quitting, it can provide restarts such as "try again" or "try a new password" or "try a different authentication method".