Mike J. Bell wrote:
On Sat, 05 Mar 2005 10:24:17 -0500, Kenny Tilton ktilton@nyc.rr.com wrote:
Mike J. Bell wrote:
(Obviously this is addressing the update cycle problem, and at the shortest path level: A depends on B depends on A (length 1), which forms a very useful group of calculations).
Cells have always grown in response to application requirements, so if someone comes up with a good example I will gladly look at implementing cycles in Cells. The question is, how can a = f(b) and b=f(a) ever be computed? In your example, there is no real cycle, because you are saying: "dollars is a function of yen /when changes/, and yen is a function of dollar /when dollar changes/." Only one can be changing at a time, so there is no cycle as the problem is conceived.
If you were to model calculations and slots *separately*, i.e. have data objects to hold the values you're worried about, and function objects that respond to changes in these data objects by changing other data objects, then you can get into update cycles.
For instance, given data objects A and B, and function objects F1 and F2, if F1 responds to changes in A and then sets B, and F2 responds to changes in B and then sets A, you could cycle infinitely without reaching steady state. A system designed with F1 and F2 is designed incorrectly. In the paradigm I'm currently using, there's a special type of function that maintains changes between A and B so as to eliminate this infinite cycle. It's actually quite simple, and you elude to the solution above: this special function is really just a tuple of the original F1 and F2, with the added semantic that the calculation code knows that if A changed, run function F1; if B changed, run function F2; if both changed, signal an error.
Yes. It is a cycle only if the system is ignorant of the full spec.
By the way, with Cells it is possible to declare a formula as "cyclic". But this is a bit of a hack. The way it works for your scenario is this:
Define an output method on slot A which sets slot B. Define an output method on slot B which sets slot A. During propagation, keep track of who is being set during any one data "pulse". If I discover, say, that A is already being set when we get another attempt to set A: If A has been defined to be cyclic, simply abandon the subsequent set operation else produce a runtime error
The bad news is that we lose the declarative thing, because the rule for each calculation is now in an output method (which is furthermore only slot-specific, where Cell rules can be instance-specific). Hmmm, maybe I should go back to the old scheme. Well, I always wait for an issue to get truly troublesome before fixing it, just to have as clear an idea as possible of the problem I am fixing.
(This is starting to expose some of the differences between what I'm using and Cells...in my system, there are distinct phases of updates apparently like in your Cells II).
But how is an instance initialized? I can supply conversion rules for dollar-balance and yen-balance, but how do I get the intial balance in there? I would need to specify a starting value, but for which one? Both? What if I specify 1 for both? That conversion would be wrong, the dollar is (for now) worth more than 1 yen.
Conceivably the answer is to let the programmer worry about it. I could specify a value (and the conversion rule) for dollar-balance and specify "unbound" and a rule for all other currencies. Then if the others get accessed, the system just calculates as usual. If all values are unbound a runtime error results. If the programmer specifies inconsistent initial values they get a wrong result (which we might be able to detect at some point of runtime) until some SETF perturbs the model and the formulas kick in to make things consistent.
Thoughts on this?
In the system I currently use, the programmer worries about it, and is only allowed to set one initial value. The system recognizes that the other slot has no value, that it *could* have a value due to this two-way conversion, and runs the appropriate function to find what it is. If the programmer sets an initial value for both slots, even if they are consistent with the function, the system signals an error.
Cool.
- Lack of values, or triggers: It's easy to come up with typed slots
that participate in calculations. However, what about untyped events? For instance, a press of a button conveys no useful state information. Is it possible in Cells to make slots dependent upon the "change" of a stateless slot? (This sort of ties back into the equals business...I understand you could implement this sort of functionality by setting a slot to, for example, a gensym uninterned symbol, so that there is no useful value and it's sure to cause everything to recalculate by any useful definition of equals, but this solution is...well, kind of ugly. It would be nice to be able to define the recalculation or "firing" semantics of a slot, which is a superset of the "equals" issue above).
I am afraid I do not follow you. My GUIs all manage buttons and other standard GUI components via Cells. A button has a "click event" slot (because clicking is surprisingly complicated) which Cello mouse-handling SETFs (so it is initialized to (c-input nil). Anyway, I am not answering your question... could you explain again what you mean by a stateless slot?
By the way, in case this helps, as a steady-state, declarative approach, Cells do make handling events tricky. I came up with the :ephemeral option to the :cell option of a slot definition:
(defmodel window .... (mouse-up-evt :cell :ephemeral :initarg :mouse-up-evt :initform (c-in nil) :accessor mouse-up-evt) .....
The way it works is this: when I SETF such a slot, the engine takes care of all propagation and then silently (not via any accessor) resets the slot value to nil.
Not sure, but I think the internals yell at you if you try to initialize to anything other than nil, such as (c-in 42).
Close?
I think I understand this...the reason you set the ephemeral slot back to nil is so that when its value changes again in the future, any slots dependent on it will definitely fire.
Not exactly. I have delta slots for that. They are understood to express a change, so each time I set a slot to any value other than a user-definable identity value, propagation takes place.
Besides, events are distinct Lisp objects (not EQL) so a new event may be EQUAL if you will, but it will be a new Lisp object and cause the rule to fire.
The reason I have ephemerals is, simply, that events are ephemeral! Consider this partial rule:
(c? (let ((evt (^mouse-down (window self)))) (when (and evt (rect-contains (^my-rect self) (mouse-pos evt)))....
Now this sequence occurs:
a mouse-down in the window, in or out of my-rect, no matter. a mouse-up occurs my-rect changes in response to any other event besides mouse-down
Well, the rule fires, because my-rect changed. The mouse button is no longer down, but we still have an old event sitting in the mouse-down slot.
We could have a nice WITH-EPHEMERAL-SETF (place value) macro, but that would end with (setf place nil), which would kick off propagation (hmm, not the end of the world, and it /would/ clear dependencies. hmmm...) or we could end with (setf (slot-value <self> <place>) nil), but then that is what ephemrals do.
Gosh, maybe :ephemeral was a mistake? If I do propagate via (setf ,place nil), then the rule fires and the dependency on MY-RECT gets cleared, as it should. And then I can eliminate the whole complexification of the ephemeral option. The downside is that ephemerality is a property of the slot (anything known to hold an OS event, for example), so if I eliminate that I then have to worry about using WITH-EPHEMRAL-SETF consistently.
Maybe the right thing is for Cells internals to handle ephemerality by propagating the reset instead of doing it silently.
In the system I use, since the data is separate from the functions, it's not *really* object-oriented. And we end up with quite a few of these data that represent "something happening." In our system, the identity of the data (i.e. which slot in which object) conveys all the information that is necessary to know when a button is pressed. Notice this is at an application level, not the GUI level. I'm sure that in the GUI internals, there's a lot of complicated information that goes into a button press...but at the system level, when you want to know, "hey, this button was pressed!"...that's essentially stateless (excepting, like I said, identity).
There are also frequent other times this comes into effect, and it all falls into a command-pattern type of deal. "I have collected data X, Y and Z...now DO this." The DO is essentially stateless.
I would imagine since we have implicit transactions and automated rollback capabilities (and all that implies...meaning phased execution cycles to propogate the "waves" of data modifications) that this "staging" of values to perform a calculation is 100% the fault of the imperative event model. If Cells is really completely declarative (like Haskell), then it's going to take me a while to wrap my brain into that model to see how they compare.
As for other questions, the latest revision of Cells (I call it Cells II) guarantees the following (I really should write this up!):
- during propagation, each cell calculates only once
- no slot accessor returns a "stale" value, by which I mean a value
which has yet to be re-calculated after some dependency direct or indirect has changed.
Interesting. That's definitely different from what I'm using. Often times data updates hundreds of times in each transaction before settling on a new value.
I got away with that for years (with many different kinds of applications) before it broke down (while doing a RoboCup soccer simulator of all things).
kt