cells
cells is a dataflow extension to CLOS (think spreadsheet-type programming, with slots being spreadsheet cells) by Kenny Tilton. It is released under a MIT-type license. Its home page is at common-lisp.net. It has undergone a major revision since it was first publicly released, and now has a significantly improved data-propagation model. On Kenny's Cells webpage, the citation for Bill Clementson's blog about Cells is valid, but Cells syntax changes have rendered the example inoperative. Updated example here

Currently supported implementations are Allegro CL, SBCL, LispWorks, CLISP, ECL, ABCL, CCL and OpenMCL. It suffers from what appears to be a bug in CMUCL's CLOS implementation, so is only partially supported there. In the past it has worked on Corman Lisp and MCL, and resurrecting this support is most likely trivial.

Find the current version and access to the mailing-list on common-lisp.net, the source via gitlab here [doesn't seem to be up-to-date]. Development occurs here.

There is a git repository with a collection of small fixes on github (for those who don't want to bother with git there is an automatically generated tarball). -- Little outdated, but supports more implementations.

There is an attempt at documentation available on github (tarball).

Notes

Understanding Cells

After trying it out a bit, my understanding is this: You can define objects whose slots trigger events when their values are modified. These slots are called "cells."

I think that's it; the rest is detail.

Two kinds of things happen when a cell's value is changed:

  • Dependencies updated

    You can specify that cell-1's value depends on cell-2. If cell-2 is changed, cell-1 will be updated.

  • Observer functions run

    You can write observer functions which are called when a cell of a given name is updated. I think only the Cells system can call them directly. (Which implies you're using them just for side-effects.)

    (Hmm, does this scale? You can make 100 objects, but each of their cells {cell-1..cell-N} will have to share observers named {cell-1..cell-N}.)

Basic example:

(defmodel my-cell () ((cell-1 :cell t :initform (c-in 1) ; c-in allows you to to modify this cell, with (for example) setf :accessor cell-1) (cell-2 :cell t :initform (c? (* (^cell-1) 2)) ; c? bars you from modifying this cell, signalling an error; it only changes when cell-1 does :accessor cell-2))) (defparameter *cell* (make-instance 'my-cell))

Cell-2 depends on cell-1's value. If cell-1 were to change, cell-2's value would change to be twice cell-1. So, let's try it out:

CL-USER> (cell-1 *cell*)
1
CL-USER> (cell-2 *cell*)
2
CL-USER> (setf (cell-1 *cell*) 10)
10
CL-USER> (cell-1 *cell*)
10
CL-USER> (cell-2 *cell*)
20

We can see that the syntax diverges a little from normal CLOS:

  • defclassdefmodel (Reasoning—syntactic sugar will be bound which normal CLOS can't accommodate.)
  • What was done by make-be is evidently handled by a shared-initialize method—but there is a not-to-be.
  • There's a :cell slot option.
  • Slots are initialized with (c-in ...) and (c? ...) forms.
  • You can refer to other cells by calling the (^cell-N) function (where cell-N is the name of another cell in the class)
[todo: discuss observers]

State vs. events

(defmodel my-model () ((cell-1 :cell t ...) (cell-2 :cell :ephemeral ...)))

cell-1 will keep its new value when changed.

cell-2, however, will revert instantly to whatever it was initialized with, after dependencies/observers on cell-2 are triggered.

(Philosophical background: From my (nonexpert) understanding, discrete processes are categorized into states and events, corresponding to :cell t and :cell :ephemeral, respectively. This is taken from Sowa's knowledge representation book.)

(The docs mention :delta, but seems unsupported.)

How one cell depends on another

Cell-1 depends on cell-2 if (and only if) cell-1 looked at cell-2 the last time cell-1's c? code ran. Kenny Tilton explained on usenet:

So dependencies will vary after every invocation of, say:

(c? (if (^a)(^b)(^c)))

between A and B or A and C.

Interestingly, this means inelegant code can create problems:

(c? (let ((b (^b))(c (^c))) (if (^a) b c)))

...always produces dependencies A, B, and C, which is a lie.

Note, btw, that dependencies are dynamic, not lexical: call a function that accesses a cell and you still get a dependency.

Special names

From Kenny Tilton's post:

  • (^slotname) - within a c? or def-c-output form, holds the current value of that slot
  • self - (look into this. only know it's bound by default within a def-c-output form to the current instance)
  • .cache - within a c? form, holds the current value of a cell
  • .parent or .pa - (fm-parent self)
  • .cause - related to deferred propagation

Also, .dpid is short for *data-pulse-id*, which is a change ID that gets incremented:

> (format t ".dpid = ~D~%cell-1 := ~D~%cell-2 = ~D~%.dpid = ~D~%" .dpid (setf (cell-1 *cell*) 48) (cell-2 *cell*) .dpid) .dpid = 1 cell-1 = 48 cell-2 = 96 .dpid = 2

Recovering from c-stop

Call (cell-reset) if c-stop is invoked. c-stop apparently halts the Cells system when circular dependencies are detected, and maybe other scenarios too.

An error message like the following may occur (formatted to be more readable):

0> c-calculate-and-set breaking on circularity | [?#:<vld>=[236]LOCATION/#<COMPUTER {41094259}>]
C-STOP> stopping because (cell ~a midst askers: ~a
                               [?#:<vld>=[236]LOCATION/#<COMPUTER {41094259}>]
                               ([?#:<vld>=[236]RESPONSE/#<COMPUTER {41094259}>]
                                [?#:<vld>=[236]LOCATION/#<COMPUTER {41094259}>]))
c-break > stopping > (cell ~a midst askers: ~a
                           [?#:<vld>=[236]LOCATION/#<COMPUTER {41094259}>]
                           ([?#:<vld>=[236]RESPONSE/#<COMPUTER {41094259}>]
                            [?#:<vld>=[236]LOCATION/#<COMPUTER {41094259}>]))

So we execute:

CL-USER> (cell-reset)
NIL