[PEAK] Lessons from the new API

Phillip J. Eby pje at telecommunity.com
Mon Apr 14 20:37:14 EDT 2008


So, I've been doing a bit of work with the new Trellis API in 
SVN.  So far, the thing I like best about it is that it removes 
"spooky action at a distance" -- having to fully define an attribute 
in one place improves readability.

I'm not too thrilled with constructs like 
'trellis.variable.attributes', which seems terribly redundant.  I'm 
thinking about changing 'variable' to 'attribute' or even 'attr', as 
it's used quite often.

But the worst part of the new API so far is that the distinction 
between 'compute' and 'maintain' isn't crystal clear to me.  In 
particular, 'compute' has two very different use cases:

1. Initializing the value of an attribute that will then either be 
writable or constant, and

2. Performing a calculation that has no visible side-effects (and can 
thus be skipped or repeated with impunity).

The first use case doesn't really fit the name 'compute', and it 
makes it harder to immediately identify what descriptor should be 
used for a given rule.

After some discussion with Grant on the #chandler IRC channel, it has 
occurred to me that for use case #1 we want something more like an 
old-school PEAK 'binding.Make'.  A Make descriptor could take a type, 
a zero, one, or 2-argument callable, or even a string designating 
something to be imported later, and use it to initialize the 
attribute, such that e.g.:

     foo = binding.Make(dict)

would result in you having an empty dictionary when you access the 
foo attribute for the first time.  You could also pass it a lambda, 
or any number of other things.

Anyway, most of the "initialization" use case for 'compute' could be 
met by adding a 'make' keyword to 'attr' (fka 'variable' fka 'value') 
so that e.g.:

    foo = trellis.attr(make=dict)

Would produce a writable attribute, initialized to an empty 
dictionary.  It would also be a nice bonus if you could say:

    @trellis.maintain(make=dict)
    def foo(self):
        ...

in order to avoid the common tedium of writing:

    @trellis.maintain
    def foo(self):
        foo = self.foo
        if foo is None:
            foo = {}
        ...

What is lost with using this syntax in place of 'compute' for the 
initialization use case, is that 'compute' can currently be used to 
dynamically create a Constant -- an attribute value that will not 
change for the life of the object instance.

This is tricky, because the *default* state under the old API was 
that rules do not produce writable cells unless you explicitly make 
them writable.  This is a performance booster in the case where a 
cell can become constant dynamically.

Anyway, the point is that in most cases where we'd use 
'attr(make=...)', the result would have been a constant under the old API.

Now, the only benefit of a constant over a regular cell is memory 
usage.  Depending on a constant doesn't create a Link object tracking 
the dependency.  Apart from that, there's no other overhead.  So, 
maybe it doesn't matter that much.  When I first implemented 
constant-izing, it was in imitation of Kenny Tilton's "Cells" package 
for Lisp, which had dependency-checking overhead for each cell used 
in a rule.  However, for some time now the Trellis hasn't used an 
algorithm with that overhead.  So, maybe there's no real point in 
worrying about it any more.

So, I guess this is my current thinking on how the current interim 
API should change:

variable            -> attr
variable.attributes -> attrs
variable.attributes.resetting_to -> resetting_attrs

compute.attributes  -> compute.attrs
maintain.attributes -> maintain.attrs

* add a 'make' keyword to 'maintain()'
* add a 'make()' attribute type, to allow you to create optional 
computed values, that can be declared writable
* add a 'make.attrs()' callable, to allow creating multiple make() attributes

In all cases the behavior of these 'make' targets would be such that 
passing in a type or a zero-argument callable would just call it, 
while single-argument functions will receive a self parameter, and so 
on -- much like PEAK's 'binding.Make()'.

This increases the vocabulary size a bit, but I think it clarifies a 
lot of things.  Many times, I find myself using trellis attributes 
simply to conveniently make something, and it should probably be 
distinguished better from repeatable (compute) and required 
non-repeatable (maintain) calculations.

I am somewhat tempted to also shorten 'compute' to 'calc' and 
'maintain' to 'ensure'.    But that's probably pushing it.  On the 
other hand, 'calc' seems a bit more like it means "side-effect 
free".  So I'm open to feedback on that, or other suggestions for how 
to name the central words.

Comments welcome.




More information about the PEAK mailing list