spacer   Trellis UserPreferences
 
spacer spacer spacer spacer spacer spacer spacer spacer spacer
 The PEAK Developers' Center   FrontPage   RecentChanges   TitleIndex   WordIndex   SiteNavigation   HelpContents 

Event-Driven Programming The Easy Way, with peak.events.trellis

(NOTE: As of 0.7a1, many new features have been added to the Trellis API, and some old ones have been deprecated. If you are upgrading from an older version, please see the porting guide for details.)

Whether it's an application server or a desktop application, any sufficiently complex system is event-driven -- and that usually means callbacks.

Unfortunately, explicit callback management is to event-driven programming what explicit memory management is to most other kinds of programming: a tedious hassle and a significant source of unnecessary bugs.

For example, even in a single-threaded program, callbacks can create race conditions, if the callbacks are fired in an unexpected order. If a piece of code can cause callbacks to be fired "in the middle of something", both that code and the callbacks can get confused.

Of course, that's why most GUI libraries and other large event-driven systems usually have some way for you to temporarily block callbacks from happening. This lets you fix or workaround your callback order dependency bugs... at the cost of adding even more tedious callback management. And it still doesn't fix the problem of forgetting to cancel callbacks... or register needed ones in the first place!

The Trellis solves all of these problems by introducing automatic callback management, in much the same way that Python does automatic memory management. Instead of worrying about subscribing or "listening" to events and managing the order of callbacks, you just write rules to compute values. The Trellis "sees" what values your rules access, and thus knows what rules may need to be rerun when something changes -- not unlike the operation of a spreadsheet.

But even more important, it also ensures that callbacks can't happen while code is "in the middle of something". Any action a rule takes that would cause a new event to fire is automatically deferred until all of the applicable rules have had a chance to respond to the event(s) in progress. And, if you try to access the value of a rule that hasn't been updated yet, it's automatically updated on-the-fly so that it reflects the current event in progress.

No stale data. No race conditions. No callback management. That's what the Trellis gives you.

Here's a super-trivial example:

>>> from peak.events import trellis

>>> class TempConverter(trellis.Component):
...     F = trellis.maintain(
...         lambda self: self.C * 1.8 + 32,
...         initially = 32
...     )
...     C = trellis.maintain(
...         lambda self: (self.F - 32)/1.8,
...         initially = 0
...     )
...     @trellis.perform
...     def show_values(self):
...         print "Celsius......", self.C
...         print "Fahrenheit...", self.F

>>> tc = TempConverter(C=100)
Celsius...... 100
Fahrenheit... 212.0

>>> tc.F = 32
Celsius...... 0.0
Fahrenheit... 32

>>> tc.C = -40
Celsius...... -40
Fahrenheit... -40.0

As you can see, each attribute is updated if the other one changes, and the show_values action is invoked any time the dependent values change... but not if they don't:

>>> tc.C = -40

Since the value didn't change, none of the rules based on it were recalculated.

Now, imagine all this, but scaled up to include rules that can depend on things like how long it's been since something happened... whether a mouse button was clicked... whether a socket is readable... or whether a Twisted "deferred" object has fired. With automatic dependency tracking that spans function calls, so you don't even need to know what values your rule depends on, let alone having to explicitly code any dependencies in!

Imagine painless MVC, where you simply write rules like the above to update GUI widgets with application values... and vice versa.

And then, you'll have the tiny beginning of a mere glimpse... of what the Trellis can do for you.

Other Python libraries exist which attempt to do similar things, of course; PyCells and Cellulose are two. However, only the Trellis supports fully circular rules (like the temperature conversion example above), and intra-pulse write conflict detection. The Trellis also uses less memory for each cell (rule/value object), and offers many other features that either PyCells or Cellulose lack.

The Trellis package can can be downloaded from the Python Package Index or installed using Easy Install, and it has a fair amount of documentation, including the following manuals:

Release highlights for 0.7a2:

Questions, discussion, and bug reports for the Trellis should be directed to the PEAK mailing list.

Table of Contents

Developer's Guide and Tutorial

Creating Components, Cells, and Rules

A trellis.Component is an object that can have its attributes automatically maintained by rules, the way a spreadsheet is maintained by its formulas.

These managed attributes are called "cell attributes", because the attribute values are stored in "cell" (trellis.AbstractCell) objects. Cell objects can be variable or constant, and either computed by a rule or explicitly set to a value -- possibly both, as in the temperature converter example!

There are five basic types of cell attributes:

Passive, Settable Values (attr() and attrs())
These are simple read-write attributes, with a specified default value. Rules that read these values will be automatically recalculated after the attribute is changed.
Computed Constants Or Initialized Values (make() and make.attrs())
These attributes are usually used to hold a mutable object, such as a list or dictionary (e.g. cache = trellis.make(dict)). The callable (passed in when you define the attribute) will be called at most once for each instance, in order to initialize the attribute's value. After that, the same object will be returned each time. (Unless you make the attribute writable, and set the attribute to a new value.)
Computed, Observable Values (@compute and compute.attrs())
These attributes are used to compute simple formulas, much like those in a spreadsheet. That is, ones that calculate a current state based on the current state of other values. Formulas used in @compute attributes must be non-circular, side-effect free, and cannot depend on the attribute's previous value. They are automatically recalculated when their dependencies change, but only if a maintenance or action-performing rule depends upon the result, either directly or indirectly. (This avoids unnecessary recalculation of values that nobody cares about.)
Maintenance Rules/Maintained Values (@maintain and maintain.attrs())
These rules or attribute values are used to reflect changes in state. A maintenance rule can modify other values or use its own previous value in a calculation. It is re-invoked any time a value it has previously used changes, even if no other rule depends upon it. Maintenance rules can be circular, as in the temperature converter example, as their values can be explicitly set -- both as an initial value, and at runtime. They are also used to implement "push" or "pull" rules that update one data structure in response to changes made in another data structure. All side-effects in maintenance rules must be undo-able using the Trellis's undo API. (Which is automatic if the side-effects happen only on trellis attributes or data structures.) But if you must change non-trellis data structures inside a maintenance rule, you will need to log undo actions. We'll discuss the undo log mechanism in more detail later, in the section on Creating Your Own Data Structures.
Action-Performing Rules (@perform)

These rules are used to perform non-undoable actions on non-trellis data or systems, such as output I/O and calls to other libraries. Like maintenance rules, they are automatically re-invoked whenever a value they've previously read has changed. Unlike maintenance rules, however, they cannot return a value or modify any trellis data.

Note, by the way, that this means performing rules should never raise errors. If they do, the changes that caused the rule to run will be rolled back, but if any other performing rules were run first, their actions will not be rolled back, leaving your application in an inconsistent state.

For each of the attribute types, you can use the plural attrs() form (if there is one) to define multiple attributes at once in the body of a class. The singular forms (except for attr()) can be used either inline or as function decorators wrapping a method to be used as the attribute's rule.

Let's take a look at a sample class that uses some of these ways to define different attributes, being deliberately inconsistent just to highlight some of the possible options:

>>> class Rectangle(trellis.Component):
...     trellis.attrs(
...         top = 0,
...         width = 20,
...     )
...     left = trellis.attr(0)
...     height = trellis.attr(30)
...
...     trellis.compute.attrs(
...         bottom = lambda self: self.top + self.height,
...     )
...
...     @trellis.compute
...     def right(self):
...         return self.left + self.width
...
...     @trellis.perform
...     def show(self):
...         print self
...
...     def __repr__(self):
...         return "Rectangle"+repr(
...             ((self.left,self.top), (self.width,self.height),
...              (self.right,self.bottom))
...         )

>>> r = Rectangle(, )
Rectangle((0, 0), (40, 10), (40, 10))

>>> r.width = 17
Rectangle((0, 0), (17, 10), (17, 10))

>>> r.left = 25
Rectangle((25, 0), (17, 10), (42, 10))

By the way, note that computed attributes (as well as make attributes by default) will be read-only:

>>> r.bottom = 99
Traceback (most recent call last):
  ...
AttributeError: can't set attribute

However, "maintained" attributes will be writable if you supply an initial value, as we did in the TemperatureConverter example. (Plain attr attributes are always writable, and make attributes can be made writable by passing in writable=True when creating them.)

Note, by the way, that you aren't required to make everything in your program a trellis.Component in order to use the Trellis. The Component class does only four things, and you are free to accomplish these things some other way if you need or want to:

  1. It sets self.__cells__ = trellis.Cells(self). This creates a special dictionary that will hold all the Cell objects used to implement cell attributes.
  2. The __init__ method takes any keyword arguments it receives, and uses them to initialize any named attributes. (Note that this is the only thing the __init__ method does, so you don't have to call it unless you want this behavior.)
  3. It creates a cell for each of the object's non-optional cell attributes, in order to initialize their rules and set up their dependencies. We'll cover this in more detail in the next section, Automatic Activation and Dependencies.
  4. It wraps the entire object creation process in a @modifier, so that all of the above operations occur in a single logical transaction. We'll cover this more in a later section on Managing State Changes.

In addition to doing these things another way, you can also use Cell objects directly, without any Component classes. This is discussed more in the section below on Working With Cell Objects.

Automatic Activation and Dependencies

You'll notice that each time we change an attribute value, our Rectangle instance above prints itself -- including when the instance is first created. That's because of two important Trellis principles:

  1. When a Component instance is created, all its "non-optional" cell attributes are calculated after initialization is finished. That is, if the attribute is a maintenance or performing rule, and has not been marked optional, then the rule is invoked, and the result is used to determine the cell's initial value.
  2. While a cell's rule is running, any trellis cell whose value is looked at becomes a dependency of that rule. If the looked-at cell changes later, it triggers recalculation of the rule that "looked". In Trellis terms, we say that the first cell has become a "listener" of the second cell.

The first of these principles explains why the rectangle printed itself immediately: the show performer cell was activated. We can see this if we look at the rectangle's show attribute:

>>> print r.show
None

(The show rule is a performer, so the resulting attribute value is None. Also notice that rules are not methods -- they are more like properties.)

The second principle explains why the rectangle re-prints itself any time one of the attributes changes value: all six attributes are referenced by the __repr__ method, which is called when the show performer prints the rectangle. Since the cells that store those attributes are being looked at during the execution of another cell's rule, they become dependencies, and the show rule is thus re-run whenever the listened-to cells change.

Each time a rule runs, its dependencies are automatically re-calculated -- which means that if you have more complex rules, they can actually depend on different cells every time they're calculated. That way, the rule is only re-run when it's absolutely necessary.

By the way, a listened-to cell has to actually change its value (as determined by the != operator), in order to trigger recalculation. Merely setting a cell doesn't cause its observers to recalculate:

>>> r.width = 17    # doesn't trigger ``show``

But changing it to a non-equal value does:

>>> r.width = 18
Rectangle((25, 0), (18, 10), (43, 10))

"Optional" Rules and Subclassing

The show rule we've been playing with on our Rectangle class is kind of handy for debugging, but it's kind of annoying when you don't need it. Let's turn it into an "optional" performer, so that it won't run unless we ask it to:

>>> class QuietRectangle(Rectangle):
...     @trellis.perform(optional=True)
...     def show(self):
...         print self

By subclassing Rectangle, we inherit all of its cell attribute definitions. We call our new optional rule show so that its definition overrides the noisy version of the rule. And, because it's marked optional, it isn't automatically activated when the instance is created. So we don't get any announcements when we create an instance or change its values:

>>> q = QuietRectangle(, left=25)
>>> q.width = 17

Unless, of course, we activate the show rule ourselves:

>>> q.show
Rectangle((25, 0), (17, 30), (42, 30))

And from now on, it'll be just as chatty as the previous rectangle object:

>>> q.left = 0
Rectangle((0, 0), (17, 30), (17, 30))

While any other QuietRectangle objects we create will of course remain silent, since we haven't activated their show cells:

>>> q2 = QuietRectangle()
>>> q2.top = 99

@compute rules are always "optional". make() attributes are optional by default, but can be made non-optional by passing in optional=False. @maintain and @perform are non-optional by default, but can be made optional using optional=True.

Notice, by the way, that rule attributes are more like properties than methods, which means you can't use super() to call the inherited version of a rule. (Later, we'll look at other ways to access rule definitions.)

Read-Only and Read-Write Attributes

Attributes can vary as to whether they're settable:

  • Passive values (attr(), attrs()) and @maintain rules are always settable
  • make() attributes are settable only if created with writable=True
  • @compute and @perform attributes are never settable

For example, here's a class with a non-settable aDict attribute:

>>> class Demo(trellis.Component):
...     aDict = trellis.make(dict)

>>> d = Demo()
>>> d.aDict
{}
>>> d.aDict[1] = 2
>>> d.aDict
{1: 2}

>>> d.aDict = {}
Traceback (most recent call last):
  ...
AttributeError: Constants can't be changed

Note, however, that even if an attribute isn't settable, you can still initialize the attribute value, before the attribute's cell is created:

>>> d = Demo(aDict={3:4})
>>> d.aDict
{3: 4}

>>> d = Demo()
>>> d.aDict = {1:2}
>>> d.aDict
{1: 2}

Since the aDict attribute is "optional" (make attributes are optional by default), it wasn't initialized when the Demo instance was created. So we were able to set an alternate initialization value. But, if we make it non-optional, we can't do this, because the attribute will be initialized during instance construction:

>>> class Demo(trellis.Component):
...     aDict = trellis.make(dict, optional=False)

>>> d = Demo()
>>> d.aDict = {1:2}
Traceback (most recent call last):
  ...
AttributeError: Constants can't be changed

And so, non-optional read-only attributes can only be set while an instance is being created:

>>> d = Demo(aDict={3:4})
>>> d.aDict
{3: 4}

But if an attribute is settable, it can be set at any time, whether the attribute is optional or not:

>>> class Demo(trellis.Component):
...     aDict = trellis.make(dict, writable=True)

>>> d = Demo()
>>> d.aDict = {1:2}
>>> d.aDict = {3:4}

Model-View-Controller and the "Observer" Pattern

As you can imagine, the ability to create rules like this can come in handy for debugging. Heck, there's no reason you have to print the values, either. If you're making a GUI application, you can define rules that update displayed fields to match application object values.

For that matter, you don't even need to define the rule in the same class! For example:

>>> class Viewer(trellis.Component):
...     model = trellis.attr(None)
...
...     @trellis.perform
...     def view_it(self):
...         if self.model is not None:
...             print self.model

>>> view = Viewer(model=q2)
Rectangle((0, 99), (20, 30), (20, 129))

Now, any time we change q2, it will be printed by the Viewer's view_it rule, even though we haven't activated q2's show rule:

>>> q2.left = 66
Rectangle((66, 99), (20, 30), (86, 129))

This means that we can automatically update a GUI (or whatever else might need updating), without adding any code to the thing we want to "observe". Just use cell attributes, and everything can use the "observer pattern" or be a "Model-View-Controller" architecture. Just define rules that can read from the "model", and they'll automatically be invoked when there are any changes to "view".

Notice, by the way, that our Viewer object can be repointed to any object we want. For example:

>>> q3 = QuietRectangle()
>>> view.model = q3
Rectangle((0, 0), (20, 30), (20, 30))

>>> q2.width = 59       # it's not watching us any more, so no output

>>> view.model = q2     # watching q2 again
Rectangle((66, 99), (59, 30), (125, 129))

>>> q3.top = 77         # but we're not watching q3 any more

See how each time we change the model attribute, the view_it rule is recalculated? The rule references self.model, which is a value cell attribute. So if you change view.model, this triggers a recalculation, too.

Remember: once a rule reads another cell, it will be recalculated whenever the previously-read value changes. Each time view_it is invoked, it renews its dependency on self.model, but also acquires new dependencies on whatever the repr() of self.model looks at. Meanwhile, any dependencies on the attributes of the previous self.model are dropped, so changing them doesn't cause the perform rule to be re-invoked any more. This means we can even do things like set model to a non-component object, like this:

>>> view.model = {}
{}

But since dictionaries don't use any cells, changing the dictionary won't do anything:

>>> view.model[1] = 2

To be able to observe mutable data structures, you need to use data types like trellis.Dict and trellis.List instead of the built-in Python types. We'll cover how that works in the section below on Mutable Data Structures.

By the way, the links from a cell to its listeners are defined using weak references. This means that views (and cells or components in general) can be garbage collected even if they have dependencies. For more information about how Trellis objects are garbage collected, see the later section on Garbage Collection.

Accessing a Rule's Previous Value

Sometimes it's useful to create a maintained value that's based in part on its previous value. For example, a rule that produces an average over time, or that ignores "noise" in an input value, by only returning a new value when the input changes more than a certain threshhold since the last value. It's fairly easy to do this, using a @maintain rule that refers to its previous value:

>>> class NoiseFilter(trellis.Component):
...     trellis.attrs(
...         value = 0,
...         threshhold = 5,
...     )
...     @trellis.maintain(initially=0)
...     def filtered(self):
...         if abs(self.value - self.filtered) > self.threshhold:
...             return self.value
...         return self.filtered

>>> nf = NoiseFilter()
>>> nf.filtered
0
>>> nf.value = 1
>>> nf.filtered
0
>>> nf.value = 6
>>> nf.filtered
6
>>> nf.value = 2
>>> nf.filtered
6
>>> nf.value = 10
>>> nf.filtered
6
>>> nf.threshhold = 3   # changing the threshhold re-runs the filter...
>>> nf.filtered
10
>>> nf.value = -3
>>> nf.filtered
-3

As you can see, referring to the value of a cell from inside the rule that computes the value of that cell, will return the previous value of the cell. (Note: this is only possible in @maintain rules.)

Beyond The Spreadsheet: "Resetting" Cells

So far, all the stuff we've been doing isn't really any different than what you can do with a spreadsheet, except maybe in degree. Spreadsheets usually don't allow the sort of circular calculations we've been doing, but that's not really too big of a leap.

But practical programs often need to do more than just reflect the values of things. They need to do things, too.

So far, we've seen only attributes that reflect a current "state" of things. But attributes can also represent things that are "happening", by automatically resetting to some sort of null or default value. In this way, you can use an attribute's value as a trigger to cause some action, following which it resets to an "empty" or "inactive" value. And this can then help us handle the "Controller" part of "Model-View-Controller".

For example, suppose we want to have a controller that lets you change the size of a rectangle. We can use "resetting" attributes to do this, in a way similar to an "event", "message", or "command" in a GUI or other event-driven system:

>>> class ChangeableRectangle(QuietRectangle):
...     trellis.attrs.resetting_to(
...         wider    = 0,
...         narrower = 0,
...         taller   = 0,
...         shorter  = 0
...     )
...     width = trellis.maintain(
...         lambda self: self.width  + self.wider - self.narrower,
...         initially = 20
...     )
...     height = trellis.maintain(
...         lambda self: self.height + self.taller - self.shorter,
...         initially = 30
...     )

>>> c = ChangeableRectangle()
>>> view.model = c
Rectangle((0, 0), (20, 30), (20, 30))

A resetting attribute (created with attr(resetting_to=value) or attrs.resetting_to()) works by receiving an input value, and then automatically resetting to its default value after its dependencies are updated. For example:

>>> c.wider
0

>>> c.wider = 1
Rectangle((0, 0), (21, 30), (21, 30))

>>> c.wider
0

>>> c.wider = 1
Rectangle((0, 0), (22, 30), (22, 30))

Notice that setting c.wider = 1 updated the rectangle as expected, but as soon as all updates were finished, the attribute reset to its default value of zero. In this way, every time you put a value into a resetting attribute, it gets processed and discarded. And each time you set it to a non-default value, it's treated as a change. Which means that any maintenance or performing rules that depends on the attribute will be recalculated (along with any @compute rules in between). If we'd used a normal trellis.attr here, and then set c.wider = 1 twice in a row, nothing would have happen the second time, because the value would not have changed.

Now, we could write methods for changing value cells that would do this sort of resetting for us, but it wouldn't be a good idea. We'd need to have both the attribute and the method, and we'd need to remember to never set the attribute directly. (What's more, it wouldn't even work correctly, for reasons we'll see later.) It's much easier to just use a resetting attribute as an "event sink" -- that is, to receive, consume, and dispose of any messages or commands you want to send to an object.

But why do we need such a thing at all? Why not just write code that directly manipulates the model's width and height? Well, sometimes you can, but it limits your ability to create generic views and controllers, makes it impossible to "subscribe" to an event from multiple places, and increases the likelihood that your program will have bugs -- especially order-dependency bugs.

If you use rules to compute values instead of writing code to manipulate values, then all the code that affects a value is in exactly one place. This makes it very easy to verify whether that code is correct, because the way the value is arrived at doesn't depend on what order a bunch of manipulation methods are being called in, and whether those methods are correctly updating everything they should.

Thus, as long as a cell's rule doesn't modify anything except local variables, there is no way for it to become "corrupt" or "out of sync" with the rest of the program. This is a form of something called "referential transparency", which roughly means "order independent". We'll cover this topic in more detail in the later section on Managing State Changes. But in the meantime, let's look at how using attributes instead of methods also helps us implement generic controllers.

Creating Generic Controllers by Sharing Cells

Let's create a couple of generic "Spinner" controllers, that take a pair of "increase" and "decrease" command attributes, and hook them up to our changeable rectangle:

>>> class Spinner(trellis.Component):
...     """Increase or decrease a value"""
...     increase = trellis.attr(resetting_to=0)
...     decrease = trellis.attr(resetting_to=0)
...     by = trellis.attr(1)
...
...     def up(self):
...         self.increase = self.by
...
...     def down(self):
...         self.decrease = self.by

>>> cells = trellis.Cells(c)
>>> width = Spinner(increase=cells['wider'], decrease=cells['narrower'])
>>> height =  Spinner(increase=cells['taller'], decrease=cells['shorter'])

The trellis.Cells() API returns a dictionary containing all active cells for the object. (We'll cover more about this in the section below on Working With Cell Objects_.) You can then access them directly, assigning them to other components' attributes.

Assigning a Cell object to a cell attribute allows two components to share the same cell. In this case, that means setting the .increase and .decrease attributes of our Spinner objects will set the corresponding attributes on the rectangle object, too:

>>> width.up()
Rectangle((0, 0), (23, 30), (23, 30))

>>> width.down()
Rectangle((0, 0), (22, 30), (22, 30))

>>> height.by = 5

>>> height.down()
Rectangle((0, 0), (22, 25), (22, 25))

>>> height.up()
Rectangle((0, 0), (22, 30), (22, 30))

Could you do the same thing with methods? Maybe. But can methods be linked the other way?:

>>> width2 = Spinner()
>>> height2 = Spinner()
>>> controlled_rectangle = ChangeableRectangle(
...     wider = trellis.Cells(width2)['increase'],
...     narrower = trellis.Cells(width2)['decrease'],
...     taller = trellis.Cells(height2)['increase'],
...     shorter = trellis.Cells(height2)['decrease'],
... )

>>> view.model = controlled_rectangle
Rectangle((0, 0), (20, 30), (20, 30))

>>> height2.by = 10
>>> height2.up()
Rectangle((0, 0), (20, 40), (20, 40))

A shared cell is a shared cell: it doesn't matter which "direction" you share it in! It's a simple way to create an automatic link between two parts of your program, usually between a view or controller and a model. For example, if you create a text editing widget for a GUI application, you can define a value cell for the text in its class:

>>> class TextEditor(trellis.Component):
...     text = trellis.attr('')
...
...     @trellis.perform
...     def display(self):
...         print "updating GUI to show", repr(self.text)

>>> te = TextEditor()
updating GUI to show ''

>>> te.text = 'blah'
updating GUI to show 'blah'

And then you'd write some additional code to automatically set self.text when there's accepted input from the GUI. An instance of this editor can then either maintain its own text cell, or be given a cell from an object whose attributes are being edited.

This allows you to independently test your models, views, and controllers, then simply link them together at runtime in any way that's useful.

Resetting Rules

Resetting attributes are designed to "accept" what might be called events, messages, or commands.

gipoco.com is neither affiliated with the authors of this page nor responsible for its contents. This is a safe-cache copy of the original web site.