[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)