Now that all of the typed containers (tuple, list, dict, set) have been implemented in the 1.0.0-dev branch, it's time to talk about observable members.
I suggest reading the following thread to get caught up on the topic:
The current state of affairs on the 1.0.0-dev branch is roughly as follows:
- Member validation and storage is fully implement and is faster and more efficient than the previous version.
- The signaling system is fully implement and is also faster and more efficient.
- Static notifications are gone; all signal handlers are registered on an Atom instance.
- All of the typed containers (tuple, list, dict, set) are fully implemented in C++.
The last major thing we need to decide on is if/how to expose member change notification.
I've previously mentioned that I thought observers (particularly static decorator observers) in the 0.x series of Atom were abused and used to implement wonky control flow. My opinion on that has not changed. I don't think I'll be convinced to support a static observer system in 1.0. Instance observers (the .observe method), however, are up for discussion. I'm looking to strike a balance in the following areas:
- Efficiency - members which are not observed should not incur any cost in time or space when changing their value
- Control - the class author should be able to control which attributes can be observed by users of the class
- Ease of use - it should not require a bunch of boiler plate code to make a member observable
- Flexibility - it should be possible for multiple members to share the same notifiers
- Implementation - the implementation should ride on top of the existing signaling system
The points have led me to the following proposals/ideas, most of which are related to Matthieu Dartiailh's idea presented in the other thread (thanks Matthieu!).
Proposal #1:
A member is marked as observable as part of it's metadata:
class Foo(Atom):
a = Int()
b = Float(42.0, observable=True)
The C++ code would create an internal Signal for each member which is marked as observable. Users would call obj.observe('b', callback) to observe any changes. With respect to the design goals:
- Efficiency - the C++ code only needs to check if the signal pointer is null before notifying (basically free)
- Control - the author has complete control over what is observable
- Ease of use - pretty simple
- Flexibility - each member has an independent set of callbacks == not very flexible
- Implementation - uses signals internally
Proposal #2:
A member is marked as observable by providing a Signal as part of it's constructor:
class Foo(Atom):
a = Int()
b = Float(42.0, notify=Signal())
This is very similar to proposal #1 except that it allows for more flexibility using the following pattern:
class Foo(Atom):
a = Int()
changed = Signal()
b = Float(42.0, notify=changed)
c = Str(notify=changed)
This would allow the user to use a single callback to listen to both 'b' and 'c'. However, that leads to a weird question: "do I use obj.changed.connect(...), obj.observe('b'), or obj.observe('c')?" In this case, they would all be equivalent.
Proposal #3:
This is a modification to #2 which eliminates the ambiguity of how things are observed. For this case, the .observe() method is removed entirely, and all connections must be performed explicitly through signals.
class Foo(Atom):
a = Int()
a_changed = Signal()
b = Float(42.0, notify=a_changed)
In this case, the class author has complete control over what signal is emitted when a member changes, and the user has a consistent interface of how they attach observers: they always connect to a signal.
The downside to all of the above approaches is that they do not give the author *full* control over how the signals are emitted. If a member value changes, the C++ code will emit the signal immediately. One can imagine cases where the author would like to batch/collapse signal notifications, or defer the notification to a later time. The above proposals would not allow for that. This leads me to the last proposal:
Proposal #4:
Give the author complete control. This approach would keep signals completely separate from members. That is, the C++ code would not emit a signal automatically on member change.
It would be the responsibility of the class author to emit a signal when a value is changed, typically by wrapping the member as a python property.
This would be more boiler plate, but would allow for the most control.
class Foo(Atom):
b_changed = Signal()
_b = Float(42.0)
@property
def b(self):
return self._b
@b.setter
def b(self,value):
if value != self._b:
self._b = value
# maybe defer
self.b_changed(value)
These are my ideas so far. They are not very concrete. Feel free to propose any suggestions, alternatives, or ideas you may have.
Chris