Introduction
Programming languages allow programmers to create definitions that bind identifiers to locations in memory that contain values. The scope of a binding is typically static (lexical). Blocks of code written within the lexical context of a definition have access to its memory location by dereferencing the associated identifier. A definition may be global or closed over by a module, class, procedure definition, etc.
Scope makes it easier to reason about where and how bindings are used. Lexical scope is intuitive because the parts of the code that have access to an identifier depend on how blocks are nested. In this example, the main() procedure shadows the global definition of cell. The definition of set_cell() is in the lexical extent of the global definition of cell. When it is called from main() the global cell is changed because set_cell() is defined outside of main(). The definition of cell in main() has a different lexical scope.
>>> cell = ['value']
>>> def get_cell():
... return cell
>>> def set_cell(value):
... cell[0] = value
>>> def main():
... cell = ['main-value']
... set_cell('new-value')
... print get_cell(), cell
>>> main()
['new-value'] ['main-value']
Using global variables like cell above is generally considered poor design. Any part of the code-base could change the value unexpectedly. This problem is amplified in multi-threaded environments. It's difficult and error-prone to enforce how global bindings are used or to reason about where changes may come from.
Sometimes using a global binding is tempting because it's simple. Certain types of bindings like an application's database connection or standard I/O ports feel global because they are used everywhere and their values rarely change. Using the singleton pattern or thread-local variables in these situations can help guarantee proper use and reduce defects due to subtle multi-threading nuances. Nevertheless, each time a global value is changed, care must be taken if it needs to be changed back.
>>> import sys, StringIO
>>> def call_with_output(stdout, proc, *args, **kwargs):
... """Temporarily change sys.stdout to stdout and call proc()."""
... orig = sys.stdout
... try:
... sys.stdout = stdout
... return proc(*args, **kwargs)
... finally:
... sys.stdout = orig
>>> def write(*args):
... print ' '.join(str(a) for a in args)
>>> out = StringIO.StringIO()
>>> call_with_output(out, write, "Hello, world!")
>>> out.getvalue()
'Hello, world!\n'
The pattern above can be expressed safely, elegantly, and consistently by using a dynamic environment of fluid bindings.
Fluid Bindings
Fluid bindings are dynamically scoped; their value depends on the runtime call-stack instead of lexical blocks. The value of a binding can be initialized to a global default and changed for the dynamic extent of a with-statement. Reasoning about a cell's use is simple because changes to the value are associated with a block construct. A cell can be guarded by making a procedure to check or adapt new values. This protects the integrity of the cell and enforces proper use.
>>> import sys, StringIO
>>> from md import fluid
>>> def writable(obj):
... assert hasattr(obj, 'write'), '%s is not writable.' % obj
... return obj
>>> output = fluid.accessor(fluid.cell(sys.stdout, writable))
>>> def write(*args):
... print >> output(), ' '.join(str(a) for a in args)
>>> write("Hello")
Hello
>>> with output(StringIO.StringIO()):
... write("world!")
... output().getvalue()
'world!\n'
>>> with output(object()):
... write('mumble')
Traceback (most recent call last):
...
AssertionError: <object ...> is not writable.
Fluid bindings exist in a dynamic environment. New threads can acquire bindings from the current environment when they are started. Unix environment variables are exported to new sub-processes in a similar way. The way bindings are acquired by new threads can be specialized by using different types of cells. This is a very intuitive and powerful abstraction of thread-local variables.
>>> import threading, thread
>>> with output(StringIO.StringIO()):
... t1 = threading.Thread(target=lambda: write('executing <id: %s>' % thread.get_ident()))
... t1.start(); t1.join()
... write('finished')
... output().getvalue()
'executing <id: ...>\nfinished\n'
Conclusion
Dynamic scope is an elegant, powerful way to parameterize the behavior of certain types of methods. The Scheme programming language, for example, allows input and output ports to be parameterized using special forms specified in R5RS. Many implementations support something like the parameter objects in SRFI-39. Common Lisp and Perl both support dynamic parameters. Bindings in Emacs Lisp are always dynamic.
The md.fluid module implements a dynamic environment with fluid bindings for Python. The md module is available on github and more examples of using fluid bindings in Python may be found in the md.fluid documentation. Feedback is welcome.
