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