Experimenting with Cells
Thursday, September 11, 2003
Kenny Tilton has been talking about his Cells implementation on comp.lang.lisp for some time but I've only just had a look at it over the past few evenings. It's actually pretty neat. Kenny describes Cells as, conceptually, analogous to a spreadsheet cell (e.g. -- something in which you can put a value or a formula and have it updated automatically based on changes in other "cell" values). Another way of saying this might be that Cells allows you to define classes whose slots can be dynamically (and automatically) updated and for which standard observers can be defined that react to changes in those slots.
Hmmm, maybe an example works best. Here's one that's a variation on one of the examples included in the latest distribution. I'll create a "motor" object that reacts to changes in the motor's operating temperature. If the temperature exceeds 100 degrees, the motor will need to be shut off. If it is shut off, the flow from the fuel pump will also need to be closed (otherwise, we get a big pool of fuel on the floor).
So, by using Cells in this example, the following will be demonstrated:
- Create slots whose values vary based on a formula. The formula can be defined at either class definition time or at object instantiation time.
- Dynamically (and automatically) update dependent slot variables (maintaining consistency between dependent class attributes).
- Create Observers that react to changes in slot values to handle "external" actions (e.g. - GUI updates, external API calls, etc.).
- Automatically filter slot changes so that we only update dependent slots when the right granularity of change occurs.
First, define the motor class (Note: defmodel is a macro that wraps a class definition and several method definitions):
(defmodel motor () ((status :cell t :initarg :status :accessor status :initform nil) (fuel-pump :cell t :initarg :fuel-pump :accessor fuel-pump :initform (c? (ecase (^status) (:on :open) (:off :closed)))) (temp :cell t :initarg :temp :accessor temp :initform (cv 0))))
Note that "status" is a cell with no initial value or formula, "fuel-pump" is a cell that has a formula that depends on the value of "status" (the ^status notation is shorthand to refer to a slot in the same instance), and "temp" is initialized to zero.
Next, define observers (this is an optional step) using a Cells macro. These observers act on a change in a slot's value. They don't actually update any dependent slots (this is done automatically by Cells and the programmer doesn't have to explicitly call the slot updates), they just provide a mechanism for the programmer to handle outside dependencies. In this example, we're just printing a message; however, in a real program, we would be calling out to something like an Allen Bradley controller to turn the motor and fuel pump on/off.
(def-c-echo status ((self motor)) (trc "motor status changing from" old-value :to new-value))
(def-c-echo fuel-pump ((self motor)) (trc "motor fuel-pump changing from" old-value :to new-value))
(def-c-echo temp ((self motor)) (trc "motor temperature changing from" old-value :to new-value))
Then, create an instance of the motor. Note that we programmatically assign a formula to the "status" slot. The formula states that when the temperature rises above 100 degrees, we change the status to "off". Since the temperature may fluctuate around 100 degrees a bit before it moves decisively one way or the other (and we don't want the motor to start turning off and on as we get minor temperature fluctuations around the 100 degree mark), we use another Cells feature ("Synapses" allow for pre-defined filters to be applied to a slot's value before it is used to update other slots) to filter the temperatures for small variations. Note that the formula is being assigned to the "status" slot at instantiation time as this gives us the ability to create different formulas for different types of motors without subclassing "motor".
(defparameter *motor1* (to-be (make-instance 'motor :status (c? (let ((filtered-temp (^temp self (fsensitivity 0.05)))) (if (< filtered-temp 100) :on :off))))))
Then we test the operation of the motor by changing the motor's temperature (starting at 99 degrees and increasing it by 1 degree +/- a small random variation).
(dotimes (x 2) (dotimes (y 10) (let ((newtemp (+ 99 x (random 0.04) -.02))) (setf (temp *motor1*) newtemp))))
This produces the following results:
0> motor temperature changing from NIL :TO 0 0> motor temperature changing from 0 :TO 98.99401 0> motor temperature changing from 98.99401 :TO 99.01954 [snipped 8 intermediate readings] 0> motor temperature changing from 99.00016 :TO 100.00181 0> motor status changing from :ON :TO :OFF 0> motor fuel-pump changing from :OPEN :TO :CLOSED 0> motor temperature changing from 100.00181 :TO 100.0177 0> motor temperature changing from 100.0177 :TO 99.98742 0> motor temperature changing from 99.98742 :TO 99.99313 [snipped 6 intermediate readings]
Kenny is using Cells in the development of a RoboCup soccer client called RoboCells for ILC2003. It'll be interesting to see how the code progresses and how the style of his Cells-based client differs from other entries.Update-2006-03-23: I have received a number of comments that the code examples on this page will not work with the current version of cells. Kenny Tilton has now included an updated version of this blog entry as an example in his cells distribution and named it "motor-control.lisp". If you want to execute the code in a current version of cells, you should use that version.