[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