Imagine a neuroscientist browsing brain data. Multiple 2D views of data are displayed; each is a projection centered on the same point in the brain. As the neuroscientist navigates in any of the views, the other views should update their images to remain consistent, i.e., centered on the same point. No matter how navigation is done - by dragging the image, scrolling, or directly entering new center coordinates, all views must remain consistent. Imagine also that the neuroscientist wants to display in the horizontal view two additional images that are 10 microns above and below the focal image. In our implementation of such a multi-view browser we would like to avoid two problems: having each view explicitly update all other ones, and remember which views are added and deleted.
The constraint-based programming model offers a good solution. This model allows the programmer to specify relationships between variables explicitly, independent of the control flow. The constraint engine enforces these relationships at runtime by updating dependent variables whenever related source variables change. These relationships are called formulas or constraints.
Our original implementation did not use constraints. Scrollbars were used to pan in x and y directions, and a scale allowed users to select another image from the stack. When moved, a scrollbar and scale widget sent the changed center coordinates to all scrollbars and scales in other views. We found our code hard to read and modify. We therefore chose to use TclProp [5] to re-implement the neighborhood viewer. With constraints, we identified a global 3D position and encapsulated each view's state in the x, y and z coordinates of its center. Since each view is the projection of the 3D space, we found it intuitive to express the x, y and z coordinates of its center as transformations of the global 3D position. The differences in data communication and Tcl code were significant and are illustrated in Figure 2.
:
Two implementations of the neighborhood viewer. In the
original implementation, each control widget must be aware of all
other ones. In the implementation with TclProp, each control widget is
linked via a constraint to a x, y, or z
coordinate of the view center. The latter are in turn linked to the
global position. Both links are two-way. Adding new views with
TclProp requires only establishing connections between the center
coordinates of the new view, and the position variable. Without
TclProp, adding new views requires updating the code for each control
widget of the other views.
The encapsulation of the view coordinates gave us the two important benefits. When the neuroscientist navigates in a view, for example, by moving a scrollbar, the scrollbar just sets the coordinates of the center appropriately. The formulas ensure that global position, and variables storing center coordinates of other views, are updated. When a view's coordinate variables change as a result of formula propagation, the constraints linking them to control widgets are triggered, and update the image in the view. For example, constraints on x and y of the view center scroll the view window in the x or y direction, respectively. The constraint on z loads a new image corresponding to the new value of z. The code became a lot simpler, since coordinate transformations were encapsulated in formulas between local and global coordinate variables, and did not have to be specified for each scrollbar or scale widget.
Second, with constraints, widget callbacks did not have to know about
widgets in other views. Scrollbars and scales communicate via
coordinate variables related by formulas. We could now add a view from
a new direction without going through the program and adding
move-notification code to each scrollbar or scale callback. The code
became easier to modify. Views were originally separate modules for
each projection plane, with a large
amount of code
overlap. Now we have merged them into a single view module, customized
by declaring appropriate formulas linking local and global coordinate
variables. Altogether, by adopting the constraints model we reduced
the number of lines of code by about a factor of two, and made the
code a lot more understandable and extensible.
More generally, we learned two important lessons about constraints:
Constraints make code simpler. If a variable is updated in several places in the program, we only need to specify once what happens after the update. During the development of the neighborhood viewer, the neuroscientists asked to implement saving position bookmarks. When the neuroscientist goes to a saved bookmark, all views should center on the point with the global coordinates stored in the bookmark. If we use constraints to relate the views' centers to a global position, we just need to implement the bookmark interface and set the global position when a bookmark is selected; the constraint engine takes care of notifying the views of the new position. This localization of the update propagation code inside constraints is what makes the code simpler and more readable.
Constraints make code easier to modify and extend. Consider the task of adding a view from another stack of images. In the traditional model, we would have to add code to each existing view that notifies the new view of the change in position. With declarative programming, we create formulas that relate the view's center coordinates to the global position coordinates. Views are unaware of one another's identity, communicating only through the constrained coordinate variables. Constraints thus allow us to focus on the part of the application being developed, while relying on the constraint engine to maintain consistency between variables.
The declarative programming model also creates new challenges to the programmer. In Tcl, constraints can be declared and executed in different contexts, so care must be taken about variable substitutions. We sometimes found it difficult to determine the program state during execution, since we had no direct control over when and how variable updates were propagated. We added extensions to TclProp to dynamically disable and enable constraints using several criteria; these extensions were useful for determining the state of the running program. We think there is a need for a ``constraint visualizer'' to help programmers evaluate and modify execution of a constraint-based program.
In sum, constraints are generally useful where relationships between
program variables can be expressed independently of the program flow,
and
where there are multiple sources of variable updates. The
declarative model is especially appropriate for event-driven programs,
which do not have a predefined execution path, and usually have
multiple ways to manipulate the program variables. The benefits of
constraints are code reduction and improved extensibility, due to
localized update propagation and hiding the information about
dependent variables inside the constraints. Networks of constraints
can be easily changed at development or runtime. We believe that the
benefits of the declarative model need to be further explored by
extending constraint packages, investigating domain areas that yield
themselves to declarative programming, and writing more
constraint-based applications.