OK, here is the promised write-up, with a scary proposed fix. What do you all think?<br><br>the code, btw, ships in broken form: the text-box will not have a complete record. Move the featuring around in the text-box rule to make an awkward fix tantamount to the fix I have in mind to for the Cells engine.
<br><br>I will follow up separately with a discussion of the proposed fix.<br><br>kenny<br><br>(defpackage #:tu-some-ephemeral-uhoh (:use :cl :utils-kt :cells :tu-cells))<br>(in-package #:tu-some-ephemeral-uhoh)<br><br>#|
<br><br>SOME over a list of ephemerals will not establish useful dependencies. Actually, I think there<br>is just a larger problem with the current implementation of ephemerals. More below.<br>The short-term fix is to force iteration over all ephemerals, then return the first found.
<br>This is because ephemerals do not change to NIL visibly to propagation --<br>it is a silent reset done by internals. Details follow:<br><br>Normally Cells and SOME get along fine. The spirit of SOME is to find just the first non-nil
<br>result in a list, returned by its predicate argument. To stay current with such an expression<br>after non-nil value V is returned by some instance F (the first instance in the list to<br>return a non-nil value), the rule should run if:
<br><br> an instance appearing earlier in the list would now return a non-nil value<br> the predicate would return a different value V2 if applied to the same instance F<br> in the special case where the new value returned by F would be nil, we want a new search
<br> down the list until (possibly) some other instance F2 returns a value.<br><br>Well, as I said, normally that works fines, assuming the predicate's return value is<br>affected only by Cells. (This, btw, is a good example of why it is hard to be "a little
<br>bit Cells".) Dependencies will exist on the population of the list of instances, and, for all <br>instances up to F, dependencies will exist on all Cells going into the predicate's derivation<br>of a value. <br><br>
If you stare at the three cases above, you will see that they all work. Note also that they work <br>even though no dependencies exist from applying the predicate to instances /after/ F. That is because<br>they do not matter until F decides to return NIL, and but that change will trigger the rule to
<br>run again and sail past F to (possibly) some new F2, establishing dependencies all along the way.<br><br>And now the problem. Suppose the predicate simply reads and ephemeral slot. Some instance F takes <br>on a value V for that slot and the rule runs. The value V gets returned, so the rule is not dependent
<br>on any instance after F. Fine. But when this propagation completes, because the slot is ephemeral, it <br>reverts to nil without propagating, which is exactly when above the rule ran again, sailed past F<br>and established dependencies of instances farther down the list, ready for someone to turn non-nil.
<br><br>When some F2 farther down the line /does/ change to return a non-nil, or even if it was ready to return<br>a non-nil when F did so it never got asked (SOME just wants the first), the rule having no dependency<br>past F will not run.
<br><br>The solution? Do not use SOME in conjunction with ephemerals. Iterate over the whole list to <br>establish dependencies and then take the first result found. if this is to inefficient, have an observer<br>on the ephemeral propagate state change via deferred SETF.
<br><br>Meanwhile, i will be looking for a fix that makes ephemerals more transparent. The only thing that springs<br>to mind (this is a preview) is re-running the rule after resetting the ephemeral just to establish <br>
the dependencies. Which is sick because the code will branch differently -- but that is the idea!<br><br>Like I said, sick. :)<br><br>|#<br>(defparameter *newline* (princ-to-string #\Newline))<br><br>(defmodel cells-chat (family) ;; kids slot can be partcipants
<br> ((text-box :initarg :text-box :accessor text-box<br> :initform (let (last-chatters)<br> (c? (let ((latest-speech<br> ;; broken...<br> (some (lambda (p)
<br> (when (speech p)<br> ;(print (speech p))<br> (cons p (speech p)))) ;; no dependencies after this<br> (^kids))
<br> ;; fixed (always hit all speeches to estab dependencies)<br> #+(or)<br> (loop with result<br> for p in (^kids)
<br> when (and (speech p) (not result)) ;; this order or same bug<br> do (setf result (cons p (speech p)))<br> finally (return result))
<br> <br> )<br> (new-chatters (set-difference (^kids) last-chatters))<br> (lost-chatters (set-difference last-chatters (^kids))))
<br> (prog1<br> (cond<br> (latest-speech<br> (destructuring-bind (p . s) latest-speech<br> (concatenate 'string
<br> (or .cache "")<br> (username p) ": " s *newline*)))<br> (lost-chatters (concatenate 'string<br> (or .cache "")
<br> (format nil "~a has/have left the chat~a"<br> (mapcar 'username lost-chatters) *newline*)))<br> (new-chatters (concatenate 'string
<br> (or .cache "")<br> (format nil "~a has/have joined the chat~a"<br> (mapcar 'username new-chatters) *newline*)))
<br> (t .cache))<br> (setf last-chatters (^kids)))))))))<br><br>(defmodel chatter (model)<br> ((username :cell nil :accessor username :initarg :username<br> :initform (error "chatter needs a `username'."))
<br> (speech :cell :ephemeral :initform (c-in nil)<br> :initarg :speech :accessor speech)))<br><br>(defobserver text-box ((chat cells-chat) new-value old-value)<br> #+confusingoutput (when new-value<br> (format t "~&--------------(text-box-of ~A)------------------:~&'~A'~%" chat new-value)))
<br><br>(defun tu-cells::tu-some-ephemeral-uhoh ()<br> (cells-reset)<br> (let* ((chat (make-instance 'cells-chat))<br> (lars (make-instance 'chatter<br> :fm-parent chat<br> :username "Lars")))
<br> (push lars (kids chat))<br> (setf (speech lars) "Cells are different.")<br> (push (make-instance 'chatter<br> :fm-parent chat<br> :username "Kenny") (kids chat))<br>
(setf (speech lars) "Hi, kenny")<br> ;<br> ; this next state change causes the text-box to lose its dependency on (speech lars)...<br> ;<br> (setf (speech (car (kids chat))) "Hi, Lars. That's for sure. Takes a while to adjust.")
<br> ;<br> ; this next state change will not be propagated...<br> ;<br> (setf (speech lars) "OK, I'll keep plugging")<br> (depart-chat lars)<br> (print (text-box chat))))<br><br><br>(defun depart-chat (chatter)
<br> (print `(departing ,chatter))<br> (setf (kids (fm-parent chatter)) (remove chatter (kids (fm-parent chatter)))))