User Tools

Site Tools


devel:design:expressions

Concept of expressions

The main reason for us to migrate Workcraft to Scala and switch to functional programming style was, beyond the obvious fascination with the novelty of functional programming and functional languages, Arseniy's idea of Expressions (which turned out to be his re-inventing the concept of Functional Reactive Programming to a certain extent, a relatively well-known concept[ 1, 2, 3, 4 etc.]).

The problem that the Expressions are designed to solve is the problem that is ubiquitous in GUI tools: the need to keep the properties of certain objects, which are dependent on the properties of other objects, up to date. Such dependencies may quickly become rather complex: for example, a simple change of a graphical location of a node in a graph-like model needs to be reflected in the following:

  • If the node is selected in the Editor, the Property Editor window needs to be updated to show the new coordinates
  • If the node has incoming or outgoing arcs, their shapes need to be updated to correctly show the arrows (they must follow the object that is being moved)
  • The image in the editor has to be updated to correctly show the new image of the model

A traditional approach widely used in imperative programming languages, particularly in Java, is to organize a system of “listeners” (the so-called “Observer Pattern”). In the observer pattern, an object whose properties need to be observed by other objects provides a subscription method which allows to register an event listener. The listener is then notified every time a property in questions changes, and the listening object immediately updates its state according to the new property value.

This pattern has numerous issues that are explained very well in this paper. The paper also proposes the replacement “reactive” pattern that is similar to the idea of Expressions.

When using Expressions, instead of operating directly on the values of the properties that change over time, an object describes its own properties (Expressions of a certain type) in terms of the properties of objects that it depends upon (other Expressions). In other words, it is no longer responsible for tracking the changes of those properties and correctly reacting to them, but has to define a set of equations that describe its behaviour relative to the behaviour of other objects.

This allows to move the responsibility of applying those equations out of the object in question, so that it is no longer the problem of a particular object how and when to recalculate the values, how to cache them and how to notify the dependent objects of the changes.

For example, to maintain the correct graphical representation of an arrow connecting two graphical objects using the Observer Pattern, the arrow object has to subscribe to the changes of location of both nodes to track their locations:

// Java
// In Arrow constructor
node1.addListener(LOCATION_PROPERTY, listener);
node2.addListener(LOCATION_PROPERTY, listener);

for efficiency the arrow object will likely need to cache the current geometry of the arrow somehow, because generating the arrow's shape may be expensive:

// Java
// In Arrow class definition
private ArrowGeometry geometry = new ArrowGeometry();

and of course it needs to update the geometry if the location of either of two nodes that it connects changed. But then the arrow needs to inform someone else that its geometry has changed so that it is correctly redrawn on the screen, and that someone must have correctly subscribed earlier to these notifications:

// Java
// In Arrow constructor
listener = new LocationListener() {
  void locationChanged(...) {
    geometry = recalculateGeometry();
    for (GraphicsChagneListener l : graphicsChangeListeners) {
      l.graphicsChanged(...);    
    }      
  }
}

Say that someone is a repaint manager that reacts to this notification by repainting the image on the screen (presumably by calling the arrow's draw method). For the image to be correct, the repaint manager has to “remember” to subscribe for the notification of each new graphical object, and to unsubscribe from objects that are being deleted to avoid memory leaks.

It is quite clear that this whole mechanism is extremely brittle. It is very easy to forget to subscribe to certain notifications or to notify the subscribers of a certain event, not to mention that in practice no one ever bothers to unsubscribe. Additionally, the objects are forced to manually cache the intermediate values if they are expensive to calculate.

When implemented using Expressions, this becomes much more robust.

So the goal is to always have an up-to-date image on the screen. That means that the image on the screen depends on the up-to-date image of every single object that is in the model.

Then the arrow has to have the following property:

// Scala
graphicalContent: Expression[GraphicalContent]

where GraphicalContent is just a function (Graphics2D ⇒ Unit) that takes a Java Graphics2D interface and produces an (imperative) code unit that draws this particular graphical content using the provided graphical interface.

The arrow's graphical content depends on the location of the two nodes that it connects, each represented by the following property:

location: Expression[Point2D]

so the arrow code looks like this:

class Arrow (location1: Expression[Point2D], location2: Expression[Point2D]) {
  val graphicalContent: Expression[GraphicalContent] = ...
}
Copyright © 2014-2024 workcraft.org

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki