[I thought I better share this with the PyCells crowd because it raises so
many interesting issues.]
In our last episode, we changed the Cells engine to rerun any rule dependent
on an ephemeral after resetting the ephemral to NIL, so that rule's
dependencies would reflect the world with the ephemeral slot as NIL. ie, it
was a consistency issue: before this, the dependencies looked as if the
ephemeral was still "on" even after it had been cleared. Inconsistency bad!
The obvious downside: the rule gets run a second time (inefficient) and god
help anyone putting side effects in their rule.
OK, save that on the stack. I turned next to something else I noticed while
playing with Lars's chat example: if a chatter was instantiated with a
non-nil speech value, that speech did not make it into the chat log. The
reason is simple:
make chat
make chatter with speech "hello world"
[speech is ephemeral, so it is gone now]
push chatter on to chat participants
the chat-log cell sees null speech for new participant
The solution brings up a Cells profundity: It is hard to be a little bit
Cells. In the extended code sample below, we arrange for a proxy "joiner"
slot to be watched by a /rule/ for the participants of a chat. Now what
happens is:
make chat
set chat joiner to be (list name opening-remark)
the participants rule runs and sees a new participant.
and creates it with the speech initialized to the
opening remark
the participants slot propagates to the chat log,
which sees the opening remark in the speech
slot of the new participant (yeahh!)
now at long last propagation has ended, and we
process the 'ephemeral-reset' queueu, clearing
the speech of the new participant
The moral is first that, yes, an instance coming into existence is extremely
interesting when it comes to internal consistency of state -- damn, we have
not just changed state but /new/ state. That moral in turn gets back to the
larger issue of it being hard to be "a little bit Cells"; the more we
express declaratively, the better our models will work.
Now let's start to look at why the article thread includes the word
"Shocker". When I implemented the above it did not work at first. The
opening remarks made it into the log, but I lost the annotations "So-and-so
has entered the chat." What happened? Hoist on my own petard!
The rule I had written for accumulating the chat log expected /either/ a
speech or a new participant or a departed participant. I had now "fixed"
things so that the new participant and its new speech were all part of one
state change to the world. Doh! OK, fix the dumb assumption:
In the chat-log rule, look for anything: someone saying something, someone
leaving, someone arriving, and check them all each time. Before I finsihed
came the Shocker: omigod. What if two new participants get added at once? Or
what if some state change propagates and causes two chat participants to
exclaim "Omigod!"? Omigod. I cannot code (some 'speech (participants chat)).
Omigod. Just as I have to check population changes /as well as/ speech acts,
i have to check all speech acts!!!! You have no idea what I am yelling
about, do you?
The last bit means I never ever should have been stopping at the first
non-nil speech anyway. It was me being smart saying to myself "I know how
Cells works, i will stop at the first". Dumb. the whole idea of (a)
declarative and (b) transparency is to write the obvious code: iterate over
all participants collecting any speech.
meaning the fix I made to ephemerals to rerun rules, as plausible as it
sounds (thine dependencies shalt match thine state) would not have been
necessary. I like plausibility, but running the rule a second time is
downright scary, let alone inefficient and unforgiving (to renegades who
modify state inside rules), so out it comes.
The really neat thing is that I am not through. The chat example eneded with
one participant leaving the chat. before I made 'participants' a rule, that
was done with an application (setf (participants chat) (remove....))
When I changed participants to be a rule, i forgot to change the depart-chat
code. It worked. I was stunned, because Cells enforces a discipline: ruled
Cells shall get their values only from an invocation of the rule. Then I
remembered. This came up a couple of days ago and when I checked the
"discipline" discovered that this rule was enforced only if *c-debug* was
bound to t. I guess my thinking was that, in this case, other qualities of
Cells still work. We do know who to notify of the change. But I was still
thinking about being mean and closing this loophole. Until now. i kinda like
this. :) There /is/ a declarative solution (change 'joiners' semantics to
admit of an opcode (:join or :depart), but come on, we can be a /little/
sloppy, right? :)
Here is the latest chat code. test function ending "oops" manifests the
problem, ending "ok" shows the fix.
kt
(defpackage #:tu-some-ephemeral-uhoh (:use :cl :utils-kt :cells :tu-cells))
(in-package #:tu-some-ephemeral-uhoh)
(defparameter *newline* (princ-to-string #\Newline))
(defmodel cells-chat (family) ;; kids slot can be partcipants
((chat-log :initarg :chat-log :accessor chat-log
:initform (let (last-chatters)
(c? (prog1
(apply 'concatenate 'string
(or .cache "")
(append
(bwhen (lost-chatters (set-difference
last-chatters (^kids)))
(list (format nil "~{~a~} has left the chat~a"
(mapcar 'username lost-chatters)
*newline*)))
(bwhen (new-chatters (set-difference (^kids)
last-chatters))
(list (format nil "~a has joined the chat~a"
(mapcar 'username new-chatters)
*newline*)))
(loop for p in (^kids)
nconcing (bwhen (s (speech p))
(list (username p) ": " s
*newline*)))))
(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)))
(defun tu-cells::tu-ephemeral-oops ()
(cells-reset)
(let* ((chat (make-instance 'cells-chat))
(lars (make-instance 'chatter
:fm-parent chat
:speech (c-in "Hi, my name is Lars")
:username "Lars")))
(push lars (kids chat))
(setf (speech lars) "Is anybody in here?")
(push (make-instance 'chatter
:fm-parent chat
:speech (c-in "Hi, my name is Kenny")
:username "Kenny") (kids chat))
(setf (speech lars) "Hi, Kenny. Cells are different.")
(setf (speech (car (kids chat))) "Hi, Lars. That's for sure. Takes a
while to adjust.")
(setf (speech lars) "OK, I'll keep plugging")
(depart-chat lars)
(print (chat-log chat))))
(defun depart-chat (chatter)
(setf (kids (fm-parent chatter)) (remove chatter (kids (fm-parent
chatter)))))
#+test
(tu-cells::tu-ephemeral-oops)
;
; PROBLEM: The log is missing the initial speeches, because the ephemerals
got reset before
; the instance was added to the particiapnts list. Solution: bring
instantiation
; within the larger dataflow by making instantiation happen inside a rule
for kids.
;
(defmodel joinable-able (cells-chat)
((joiners :cell :ephemeral :initform (c-in nil)
:initarg :joiners :accessor joiners))
(:default-initargs
;
; SOLUTION: the initial speech will stay around until the entire state
change
; propagation has completed, including updating the chat log which has
; a dependency on the chat 'kids'. The new chatter gets instantiated
as
; part of this rule, which then propagates to the chat-log, which sees
the
; speech. That is ephemeral, but ephemerals do not get reset until
after
; all propagation in one datapulse (ephemeral reset has its own
queueu).
;
:kids (c? (the-kids
(loop for (name opening-remark) in (^joiners)
collecting (make-kid 'chatter
:username name
:speech (c-in opening-remark)))
.cache))))
(defun join-chat (chat &rest names)
(setf (joiners chat) (loop for n in names
collecting (list n (format nil "Hi, my name is
~a" n))))
(subseq (kids chat) 0 (length names)))
(defun tu-cells::tu-ephemeral-ok ()
(cells-reset)
(let ((chat (make-instance 'joinable-able))
lars kenny)
(setf lars (car (join-chat chat "Lars")))
(setf (speech lars) "Is anybody in here?")
(setf kenny (car (join-chat chat "Kenny")))
(setf (speech lars) "Hi, Kenny. Cells are different.")
(setf (speech kenny) "Hi, Lars. That's for sure. You seem to be coming
up to speed nicely.")
(setf (speech lars) "OK, I'll keep plugging")
(depart-chat lars)
(join-chat chat "Peter" "Paul" "Mary")
(print (chat-log chat))
(values)))
#+test
(tu-cells::tu-ephemeral-ok)