[PEAK] Re: Trellis: undetected circularity via optional rules

Sergey Schetinin maluke at gmail.com
Thu Apr 16 10:58:03 EDT 2009

I really think this should be solved by separating the settable and
read-write cells, so I propose that:

1. @maintain rules would not really be cells -- they would be
listeners but not subjects, wouldn't have a value and the Component
initializes them last. They would read some cells and write some
others -- no return value, no listeners, no init on access. They might
be optional, but I don't see a use case for that.
2. new @track rules would essentially be like current @maintain rules
but run in read-only mode (can't change other cells), and would be
capable of not being writable.

This resolves some of the reported issues and, if I am not mistaken,
allows to simplify the cell initialization machinery. I don't think
there are any valid use cases where the cell must both have a value
and change some other cells.

Another good change I think would be to drop support for cells which
can compute a value but be overriden by an assignment (currently
@maintain, @track in proposal above), it does seem fit DAG model but
it would be more cleanly expressed by two separate cells. There's
still a good reason to want to assign to attributes that correspond to
computed cells, but I think it should be handled differently from an
implementation standpoint. I see a couple of ways to represent that.
I'll show them on TempConverter example:

This is the best conversion IMO because it's concise and if
filter_attr is implemented as a separate cell it would catch
inconsistent sets like TempConverterV1(C=0, F=0).

to_c = lambda f: (f - 32) / 1.8
to_f = lambda c: c * 1.8 + 32

class TempConverterV1(Component):
    C = attr(0)
    F = filter_attr(C, to_c, to_f)
    # or F = filter_attr('C', ...

class TempConverterV2(Component):
    F = attr(0)
    C = compute(lambda self: to_c(self.F))
    def _set_c(self, val):
        self.F = to_f(val)

class TempConverterV3(Component):
    F = attr(0)
    C = compute(lambda self: to_c(self.F), write_to='_set_C')
    _set_C = attr(resetting_to=None)
    def _on_set_C(self):
        if self._set_C is not None:
            self.F = to_f(self._set_C)

class TempConverterV4(Component):
    F = attr(0)
    C = compute(lambda self: to_c(self.F), write_to='_set_C')
    def _set_C(self, value):
        self.F = to_f(value)
    _set_C = property(fset=_set_C)

The next one is too verbose and when the _set_* value resets there's
an unnecessary recalc, however see a variation after that

class TempConverterV5(Component):
    attrs.resetting_to(_set_F=None, _set_C=None)

    @track(initially=32, write_to=_set_F)
    def F(self):
        if self._set_F is not None:
            assert self._set_C is None
            return self._set_F
            return to_f(self.C)

    @track(initially=0, write_to=_set_C)
    def C(self):
        if self._set_C is not None:
            return self._set_C
            return to_c(self.F)

If we hide the above machinery and make sure there's no recalc after
(internal) _set_* resets, then we'd get the following:

class TempConverterV6(Component):
    def F(self):
        return to_f(self.C)

    def F_filter(self, value):
        assert self._set_C is None
        return value

    def C(self):
        return to_c(self.F)

    def C_filter(self, value):
        return value

This allows us to filter / validate assign values. Filters can also
discard the assigned value and use the computed one by referencing the
cell, this may work in many ways one of which would be that when
assigned dependencies of the cell are preserved another would be that
dependencies are replaced with dependencies created by _filter
function, so if it does not reference the original cell it would act
as compute until assigned, but depending on login in filter_assign
handler some other assign later might resume the computation. I see
how this can be useful for things like computed defaults, consider

class Window(Component):
    def align(self):
        return self.parent.align

    def set_align(self, value):
         if value is DEFAULT:
             return self.parent.align
         return value

The Window().align = LEFT would override the computed align and .align
= DEFAULT would restore the original logic

If we add some magic, so that was_set_to creates a dependency that is
conditioned to be triggered if only if the dependent is not the
setter, this would work:

class TempConverterV7(Component):
    attrs(C=0, F=32)
    _set_F = F.was_set_to
    _set_C = C.was_set_to

    def _maintain(self):
        if self._set_F is not NOT_SET:
            assert self._set_C is NOT_SET
            self.C = to_c(self._set_F)
            self.F = self._set_F
        elif self._set_C is not NOT_SET:
            self.C = self._set_C
            self.F = to_f(self.C)

Yet another option would be

class TempConverterV8(Component):
    def F(self):
        if self._set_C is not NOT_SET:
            assert self._set_F is None
            return to_f(self._set_C)
        return self.F

    def C(self):
        if self._set_F is not NOT_SET:
            return to_c(self._set_F)
        return self.F

    _set_F = F.was_set_to
    _set_C = C.was_set_to

Anyway, please consider the first proposal separately, the rest is
more of a brain dump.

More information about the PEAK mailing list