[PEAK] Laziness, terminology, and the Trellis API

Phillip J. Eby pje at telecommunity.com
Thu Mar 13 20:16:53 EDT 2008


While working on an updated roadmap/release plan for the Trellis, I 
ran across a couple of features I had on the backburner, that I 
thought I should dive into first before finishing the main post, so I 
can get some feedback.

Right now, we support only one kind of laziness in the Trellis.  If a 
component attribute is marked "optional", then its cell is not forced 
into initialization when the component is created.

However, as we construct larger and more interesting Trellis 
applications, there may be occasions where we want a bit more laziness.

In other words, just as our new "external connection" capability will 
either subscribe to something or not, depending on whether there are 
any listeners, we may also want to have rules that are only updated 
if they have listeners.

Such rules would have to be side-effect free, of course, since 
otherwise the rest of the system wouldn't get updated.  (In a sense, 
rules that make changes to the system *have* listeners, just not 
trackable ones.)

However, I can see wanting to not update every menu item in an 
application to reflect whether it's enabled, every time you change 
the selection, for example.  Those updates would only need to take 
place when popping up the relevant menus.  For example, with a rule like this:

     if menu.is_active:
         for item in menu.items:
             item.enabled = item.model.enabled

Then, the 'enabled' flags for the interaction components would only 
be automatically recalculated while the menu was popped up.  The rest 
of the time, the cells would simply be marked dirty.  (Of course, 
toolbar buttons or other visible indicators of command enablement 
would still have these things recalculated.)

I think it's reasonable to assume that laziness implies 
optionalness.  That is, it shouldn't be necessary to mark something 
as both lazy *and* optional; there's no reason to force a lazy cell 
into being before it has been explicitly read.  There's also no 
reason to make a rule with no side-effects optional: you could just 
make it lazy.

All in all, there should be three kinds of rules:

1. Eager Rules (w/side-effects)
2. Optional rules w/side-effects
3. Lazy rules (optional, no side-effects)

And two kinds of observers (optional, and non-optional).

I kind of wonder here, if our terminology needs work.  This reminds 
me of the big renaming of peak.binding's descriptors, back in 
2003.  (Wow, has PEAK really been around that long?  Longer, actually...)

Anyway, back then, we had a whole bunch of keywords in the 
peak.binding API for creating different kinds of attributes, and it 
needed to be rethought because there wasn't a harmonious pattern that 
made obvious sense.  All the terms had "just grown" as the library 
developed, much like has been happening with the Trellis.  As I wrote 
back then (9/2/2003):

"""But feedback from Ty and Lynn suggested that what we really need 
to emphasize is *why* bindings are; i.e., what they are for.  Neither 
the current spelling nor my first proposal addressed this very well."""

So, I think that we need to do a similar rethinking of the API 
terminology.  We have terminology that mostly reflects what things 
*are*, e.g. rules and values and optional and so on, but not so much 
WHY things are.  What are the rules for?  Why do you need values?  Etc.

For example, if we called a "value" a "variable" instead, it might 
emphasize the fact that it's a read/write value, not just that 
there's a value.  That is, since it's a variable, you can store things in it.

And why do we have rules?  Well, we would use a lazy rule to compute 
a value with no side-effects or reference to a previous state.  We 
would use a plain rule when we want to maintain a value, tracking 
every update.  We would use an optional rule for the same thing, 
except that we don't want to start before we have to, or maybe won't 
do it at all.

Observers maintain an external condition, and optional observers maybe do it.

Are there words to distinguish between maintaining something 
internal, and maintaining something external?  They're almost like 
the difference between thinking and doing.

Some random words for ideas: compute, maintain, react, track, update, 
observe, action, notify, respond, effect, calculate, live, active, 
report, kicker, doer, does, doing, do, run, call, invoke, reflect, 
display, output, manifest, make, materialize, transmit, deliver, 
demonstrate, deliverable, courier, carry out, dispatch, announce, 
command, pronounce, proclaim, evoke, emit, emitter, perform...

So out of all that, I kind of like:

* variable - hold a value that can be set
* compute - calculate a value, no side-effects (i.e., lazy/cached)
* maintain - keep one or more values current within the Trellis
* maybe_maintain - optionally maintain
* perform - do actions in the outside (non-Trellis) world
* maybe_perform  - optionally perform

Of course, the first four items above can also be discrete 
(transient), so we also need:

* transient_variable - (called "receiver" today)
* compute_transiently  - (@discrete, except also lazy)
* maintain_transiently - (@discrete today)
* maybe_maintain_transiently - (@optional @discrete today)

This is 10 things, not counting the existing @todo (which probably 
doesn't need to change).  It also ignores the need to specify default 
values for maintain/compute variants, and the need to specify whether 
a computed or maintained value is also variable (i.e., writable).

Sigh.  This one of those rare times that I envy Haskell and Ruby for 
their ability to string words together and look like a DSL.  Because 
then we could do stuff like:

     foo = writable transient lambda self: ...

Of course, for transients we need to be able to specify what the 
value resets to...  so we could actually do:

* variable(resets_to=...)
* compute(resets_to=...)
* maintain(resets_to=...)
* maybe_maintain(resets_to=...)

I'm not all that crazy about 'resets_to'; it'd be nice to have a 
shorter way to say it.

For default or initial values, we could use an 'initially' keyword, 
and it would be the first positional argument for variable:

* variable(...)
* compute(initially=...)
* maintain(initially=...)
* maybe_maintain(initially=...)

This doesn't handle writability, though.  But, perhaps we could say 
that if you use 'initially', then you're writable unless you say 
you're readonly.  E.g.:

* compute.readonly(initially=...)
* maintain.readonly(initially=...)
* maybe_maintain.readonly(initially=...)

To indicate that even though you're specifying an initial value, you 
don't want the cell to be writable at runtime.

Hm.  If we're doing that, though, perhaps we should have an 
'optionally' namespace, containing its own versions of 'perform' and 
'maintain':

* optionally.perform
* optionally.maintain
* optionally.maintain.readonly

Thus bringing the full list of singular APIs to:

* trellis.variable
* trellis.compute
* trellis.maintain
* trellis.maintain.readonly
* trellis.perform
* trellis.optionally.maintain
* trellis.optionally.maintain.readonly
* trellis.optionally.perform
* trellis.todo
* activity.task

And for the namespace plural versions:

* trellis.variable.attributes(name=value, ...)
* trellis.variable.attributes.resetting_to(name=value, ...)
* trellis.compute.attributes(name=lambda self:..., ...)
* trellis.todo.attributes(name=lambda: self:..., ...)

It certainly seems tempting, however, to just do:

     from peak.events.trellis import *

So as not to have to keep typing "trellis." a bajillion times.  :)

At this point, we also still haven't covered the other new features 
in progress, i.e., external subscriptions and writebacks.  However, 
we could define those using additional decorators, e.g.:

@trellis.compute
def someattr(self):
     # get data from external source

@someattr.connect
def someattr(self, cell):
     # arrange for cell.receive(value) to be
     # called if external source changes
     # return a key for unsubscriber to use to cancel

@someattr.disconnect
def someattr(self, key):
     # unsubscribe the callback designated by key

@someattr.transmit
def someattr(self, value):
     # send value to the external source

Anyway, that's my rough sketch so far of how I think I would extend 
and refactor today's API.  The key terms to learn are:

Basic nouns/verbs: variable, @compute, @maintain, @perform
Value/state control: initially=, resets_to=, .readonly, optionally.
External connnection: @.connect, @.disconnect, @.transmit
Miscellaneous: @task, @todo, attributes(**kw)

That's only 14 words, but they can be combined in roughly 150 ways, 
and I think the implementation will actually be a lot simpler than 
the current crazy API that tries to guess what you mean by what 
combination of explicit and inherited things you throw together.  In 
addition, with this API it should be possible to make it so you can 
refer to superclass versions of an attribute definition, for explicit re-use.

Anyway, I just wanted to toss this out there to get some feedback, 
see if anybody has any comments, questions, complaints, etc.  :)




More information about the PEAK mailing list