[closer-devel] ContextL News (incl. first-class dynamic environments!!!)
Pascal Costanza
pc at p-cos.net
Sat Feb 7 14:47:23 UTC 2009
Hi,
I have had some working code for first-class dynamic-environments for
a few weeks by now, but have only come around to releasing it to the
ContextL darcs repository today. The reason is that this is a
relatively major change in the implementation of ContextL, so I wanted
to be extra careful (also with regard to coming up with an interface
to dynamic environments that I think is ready for prime time).
I am not going to make this an official release yet, because there may
still be feedback from people who want to try the new features out. So
I would like to leave some time for integrating such feedback before
finalizing things.
Apart from the first-class dynamic environments, there are two more
important changes that may or may not affect ContextL users:
- I have changed the implementation for ensuring internal unique names
so that it doesn't use a "stop the world" synchronization lock
anymore, to make it work better on multicore architectures. However,
this means that ContextL now depends on trivial-garbage (which is asdf-
installable).
- I have removed the asdf definitions for tweaking the layer garbage
collector, and also changed the other ways of removing optional
features to a more uniform model. See contextl.asd for more details.
From now on, ContextL doesn't by itself add anything to *features*
anymore, but you can add keywords to *features* to remove optional
features from ContextL before compiling it. (You can remove the
support for first-class dynamic environments, the extra checks when
rebinding special places, and the layer garbage collector.)
There is now also a testsuite specifically for Clozure Common Lisp.
Now to the really interesting stuff, the first-class dynamic
environments:
The core of the extension are the operators dynamic-wind, capture-
dynamic-environment, with-dynamic-mark, and with-dynamic-environment.
Dynamic-wind can be used to add a certain action to the current
dynamic environment, delimited by a nested proceed form. Here is an
example:
(dynamic-wind
(handler-case
(proceed (do some stuff))
(error () ...)
(warning () ...)))
This example sets up exception handlers in the dynamic environment for
the code enclosed in the proceed form. If somewhere in its dynamic
extent some code calls capture-dynamic-environment (that therefore
dynamically closes over this particular piece of code), then all the
actions except what's in the proceed form are captured in the
respective representation of the dynamic environment. Whenever such a
representation of the dynamic environment is reestablished (using with-
dynamic-environment), the exception handlers are set up again for the
new code. So for example:
(defun foo () (capture-dynamic-environment))
(setq *env* (dynamic-wind
(handler-case (proceed (foo))
(error () (format t "I caught an error.")))))
(with-dynamic-environment (*env*)
(error "boo!"))
This will catch the error "boo!" with the exception handler that
prints "I caught an error.", because the with-dynamic-environment form
reestablished the exception handler from the previous form.
You can also understand this as if the with-dynamic-environment form
is replaced by the following code:
(handler-case (error "boo!")
(error () (format t "I caught an error.")))
...that is, everything from the dynamic environment is called, except
the proceed forms which are replaced by the body of with-dynamic-
environment. However, the code that is executed by a with-dynamic-
environment is not determined statically but depends on the dynamic
value of the respective environment object, which in general can be an
arbitrary long chain of dynamic winds all nested inside each other.
The portion of the dynamic environment that is captured can be
delimited by a dynamic mark. For example:
(defvar *mark*)
(defun foo ()
(dynamic-wind (print 'foo) (proceed (bar))))
(defun bar ()
(with-dynamic-mark (*mark*) (baz)))
(defun baz ()
(dynamic-wind (print 'baz) (proceed (bam))))
(defun bam ()
(capture-dynamic-environment *mark*))
(setq *env* (foo))
(with-dynamic-environment (*env*)
(print 'duh))
This prints BAZ and DUH, but not FOO because it is not in the part
delimited by *mark*. Such delimited dynamic environments should be
especially useful in conjunction with partial continuations: You mark
the code that is the top of the part of the computation that will be
captured in a partial continuation, and when you actually capture the
continuation, you also capture the dynamic environment up until that
mark. You then only need your own way of 'bundling' both the
continuation and the dynamic environment. [Since ContextL doesn't
provide support for continuations itself, ContextL doesn't do this for
you.] with-dynamic-mark can bind both lexical and special variables,
so it can be used both for lexically and dynamically scoped partial
continuations. (This part of the design is highly inspired by http://okmij.org/ftp/Computation/dynamic-binding.html#DDBinding
)
More often than not, dynamic-wind is too low-level. To abstract over
dynamic-wind, ContextL provides a reimplementation of special
variables and special places in terms of dynamic-wind so that they are
automatically captured with capture-dynamic-environment. The same is
true for layer activations and deactivations.
For special variables, I have followed the design of dynamic-variables
in ISLISP. So instead of using defvar, defparameter, let and let*, you
have to use defdynamic, the 'dynamic' operator for looking up values,
and dynamic-let and dynamic-let* (as well as dlet and dlet* as
abbreviations). I'm not doing this to annoy you ;), but providing new
binding forms is actually necessary to be able to hide the
implementation details when expressing these things in terms of
dynamic-wind (but it is true that I like ISLISP's design for dynamic
variables a lot).
Here is a very simple example:
(defdynamic value 42)
(defun foo () (print (dynamic value)))
(defun bar () (dynamic-let ((value 4711)) (foo)))
(foo) => 42
(bar) => 4711
In conjunction with dynamic environments, it is important to be able
to make a particular distinction in dynamic binding forms: whether you
want a binding to be recomputed every time a dynamic environment is
reestablished, or whether you want exactly the same binding.
Therefore, the binding forms exist in two different versions: dynamic-
relet and dynamic-relet* (and their abbreviations drelet and drelet*)
will recompute bindings every turn, and dynamic-let and dynamic-let*
(and their abbreviations dlet and dlet*) will use exactly the same
bindings.
Here are two examples to illustrate the different semantics. First for
recomputation semantics:
(defdynamic value '(d e f))
(setq *env* (dynamic-relet ((value (append '(a b c) (dynamic value))))
(capture-dynamic-environment)))
(with-dynamic-environment (*env*) (dynamic value)) => '(a b c d e f)
(setf (dynamic value) '(4 5 6))
(with-dynamic-environment (*env*) (dynamic value)) => '(a b c 4 5 6)
The reason for the different results is that for each reestablishment
of *env*, the append of the respective lists will recomputed over and
over again for determining the binding of (dynamic value).
Now an illustration for reusing the same binding:
(defdynamic value 0)
(setq *env* (dynamic-let ((value (incf (dynamic value))))
(capture-dynamic-environment)))
(dynamic value) => 1
(with-dynamic-environment (*env*) (dynamic value)) => 1
(dynamic value) => 1
(with-dynamic-environment (*env*) (incf (dynamic value))) => 2
(dynamic value) => 1 ; !!! it's a different binding !!!
(with-dynamic-environment (*env*) (dynamic value)) => 2 ; !!! old
binding reestablished !!!
So it's indeed the case that with dynamic-let and dynamic-let*, the
bindings will be captured in the dynamic environments, not the values.
This also means that if you pass a dynamic environment to a different
thread in a multithreaded system and reestablish it there, you may
have shared state between different threads whose accesses you may
have to synchronize accordingly (by using locks, or so).
[A design question I am not 100% sure about at the moment: In the
current version, defdynamic maps to defparameter internally, and there
is no equivalent form for defvar. I don't know whether both are
necessary, and/or which one should be the default one. I'm interested
in opinions here.]
ContextL provides a first-class representation of such dynamic
variables called dynamic symbols, which you can use like regular
Common Lisp symbols. However, since they are _not_ regular Common Lisp
symbols, you have to use dynamic-symbol-value, dynamic-symbol-boundp,
dynamic-symbol-makunbound, etc., instead of symbol-value, boundp,
makunbound, etc. You can check whether something is a dynamic symbol
with dynamic-symbol-p. The rebinding forms are dynamic-progv (for
binding-reuse semantics) and dynamic-reprogv (for binding-recompute
semantics). The special symbols to express special places are now
essentially also such dynamic symbols.
with-active-layers and with-inactive-layers are now also expressed in
terms of dynamic-wind, and they use the binding-recompute semantics.
So whenever you say (with-active-layers (l1) ...) and capture a
dynamic environment in the dynamic extent of this layer activation,
layer l1 will be reactivated on top of any other active layers
whenever the respective dynamic environment is reestablished using
with-dynamic-environment. Likewise for (with-inactive-layers (l1) ...)
- which ensures that l1 will be inactive whenever the respective
dynamic environment is reestablished.
You can take a look at all these higher-level operators (dynamic-let,
dynamic-relet, dynamic-progv, dynamic-reprogv, with-activer-layers and
with-inactive-layers) to see how you can implement such macros
yourself on top of dynamic-wind. Special attention should be payed to
giving the local proceed operators generated names [thanks a lot to
Tobias, this proved to be an essential ingredient in the
implementation!!!]. Here is template how to do this:
(dynamic-wind :proceed my-proceed
... (my-proceed ...) ...)
Here, my-proceed is a name for the proceed form given by the user,
which can be a gensymmed name in macro definitions. If you define your
own proceed name, ContextL's 'proceed cannot be used for that
particular dynamic-wind anymore (to be able to have nested dynamic-
winds).
There is a test suite for dynamic environments called dynenv.lisp
which may throw more light on the semantics of the new features.
If for some reason, you don't want to use first-class dynamic-
environments, you can push :cx-disable-dynamic-environments to
*features* before compiling ContextL. You can then still use dynamic-
wind and with-dynamic-environment (which are then essentially noops),
but not capture-dynamic-environment or with-dynamic-environment.
Dynamic symbols will then be plain, uninterned Common Lisp symbols,
and dynamic-let and dynamic-relet will be shallow wrappers on top of
Common Lisp's special variable binding forms (and not have different
semantics anymore, of course ;). If it is indeed the case that you
want to disable first-class dynamic environments, I would like to know
about it, so please contact me in that case.
Ah, a final note, to avoid confusions: ContextL's dynamic-wind is
inspired by Scheme's dynamic-wind, but apart from the fact that
ContextL's dynamic-wind is not thrown together with call/cc, there is
also the important distinction that ContextL's dynamic-wind doesn't
perform an unwind-protect automatically. If you want that, you have to
say something like this:
(dynamic-wind
(unwind-protect
(proceed ...)
(clean-up)))
[I strongly believe in not overloading language constructs with too
many semantics at the same time, so I prefer to keep the unwind
protection separate from the rest of the dynamic environment semantics.]
OK, if you have any questions, please shoot!
Pascal
--
ELS'09: http://www.european-lisp-symposium.org/
Pascal Costanza, mailto:pc at p-cos.net, http://p-cos.net
Vrije Universiteit Brussel
Programming Technology Lab
Artificial Intelligence Lab
Pleinlaan 2, B-1050 Brussel, Belgium
More information about the closer-devel
mailing list