Thursday, February 14, 2008

Arc!Cells: Baby Steps

[This first article on doing Cells (see the "Cells Manifesto" entry in this blog) in Arc serves more to motivate the idea than anything else. In a nutshell, we want to be able to do something like this:

(= a (ci 3))
(= b (ci 4))
(defobserver c ()
(prs "Hey, C is now" new-value
"was" old-value))
(= c (c? (sqrt (+ (expt (cval a) 2)
(expt (cval b) 2)))))
Hey, C is now 5 was unbound
(cset a 6 b 8)
Hey, C is now 10 was 5

In other words, values are not just functions of values but also active bits of state which can have arbitrary observer logic bound to them.

In a subsequent (next?) blog I plan to easy up on the tutorial angle and just present a decent port of what I called Cells II, which was powerful but had theoretical holes closed in Cells3.

A close version of what follows is in CVS here (as cells-1.arc in case this link does not fly):

http://common-lisp.net/cgi-bin/viewcvs.cgi/kennysarc2
/cells-1.arc?rev=1.1&root=cells&view=log

Enjoy.]

First a couple of utilities:

(def prt args
(apply prs args)
(prn))

(mac prun (banner . forms)
`(do (prn ,banner)
(prn '(do ,@forms))
(prn)
,@forms))

Let's start with the moral equivalent of a C++ member function, or at least I think that's what they call it. Something that looks like a stored data member or slot of an attribute of an object but is in fact implemented as a function of that object.

An example would be having a rectangle object with data members where one could store length and width and then have an area attribute implemented (in C++) as:

area = this.length * this.width;

Aside from saving a little memory, one gets a guarantee that the area will always be consistent with the length and width, which is not the case if one is writing code that says oh gosh I just changed the length I better go change the area.

As our 'application pushing down on the core' we'll use my favorite, a furnace boiler.

(= b* (obj outside-temp 72
on? [< _!outside-temp 50])))

No, the outside temp is not an attribute of a boiler, we're just keeping things in one table as a convenience until we get the ball rolling, later on we'll deal with multiple objects.

That anonymous function above boils down to:

If the outside temp is less than 50,
then turn on the boiler,
otherwise turn it off.

First, let's see if the rule works (not a big accomplishment)

(prt 'boiler b*!outside-temp (if (b*!on? b*) 'on 'off)))

-> boiler 72 off

Good, now change temp to 32 and see if the boiler comes on:

(= b*!outside-temp 32)
(prt 'boiler b*!outside-temp (if (b*!on? b*) 'on 'off)))

-> boiler 32 on

Super. Now let's hide the fact that on? is a function
behind a reader function:

(def on? (i) (i!on? i)))

...and ease inspection:

(def pr-boiler (b)
(prt 'boiler 'temp b*!outside-temp (if (on? b) 'on 'off)))

Test new slot reader, setting temp high enough this time
so that the boiler should go off:

(= b*!outside-temp 80)
(pr-boiler b*))

-> boiler temp 80 off

But we want more flexibility than having an attribute always defined by a function. Maybe we just want to store nil or t in on? and maintain it as usual, via assignment. Now on? can no longer be assumed to be a function. Fortunately we already have it behind a reader in our burgeoning little OO system, so we just need to enhance that (and get a redefinition warning):

(def on? (i)
(awhen i!on?
(if (isa it 'fn)
(it i)
it)))

Just test that our new accessor for on?:

(= b* (obj outside-temp -10
on? nil)) ;; we'll ignore the outside-temp for this test
(pr-boiler b*))

boiler temp -10 off

Now assign t to on?:

(= b*!on? t) ; We'll hide the assignment implementation later.
(pr-boiler b*))

boiler temp -10 on

Good. We will want all our attributes to work this way, so we may as well generalize the on? behavior now:

(def slot-value (i slot-name) ;; i is like self ala Smalltalk
(awhen i.slot-name
(if (isa it 'fn)
(it i)
it))))

(mac defslot (name)
`(def ,name (i) (slot-value i ',name)))

(defslot outside-temp)
(defslot on?)
(defslot inside-temp) ;; Let's start elaborating the model

(def pr-boiler (i)
(prt 'boiler
'outside-temp (outside-temp i)
(if (on? i) 'on 'off)
'inside-temp (inside-temp i)))

And test:

(= b* (obj outside-temp 20
on? nil
inside-temp [if (on? _)
72
_!outside-temp])))

boiler outside-temp 20 off inside-temp 20

Now let's bring back the automatic boiler:

(= b*!on? [< _!outside-temp 50]))

...and step the outside temperature up from freezing to torrid.

(loop (= b*!outside-temp 30)
(< b*!outside-temp 100)
(= b*!outside-temp (+ b*!outside-temp 10))
(pr-boiler b*)))

Looks like we need an air conditioner. And let's get more realistic about the model

(= outside* (obj temp 20))
(defslot temp)

(= furnace* (obj on? [< (temp outside*) 50]))
(= ac* (obj on? [> (temp outside*) 75])) ;; air conditioner
(= inside* [if (on? furnace*) 72
(on? ac*) 68
(temp outside*)])

(def dumpworld ()
(prt "outside" (temp outside*))
(prt "furnace" (if (on? furnace*) 'on 'off))
(prt "a/c" (if (on? ac*) 'on 'off))
(prt "inside" (temp inside*)))

Step temperature up from freezing to torrid, but with an air-conditioner:

(loop (= outside*!temp 30)
(< outside*!temp 100)
(= outside*!temp (+ outside*!temp 10))
(prn)
(dumpworld)))

Nice. We have built a working model that runs by itself given simple declarative rules, meaning we state the rules and an engine sees to it that the model runs. But we have a problem. Let's add a debug option to our slots:

(def slot-value (i slot-name (o debug))
(awhen i.slot-name
(if (isa it 'fn)
(do
(when debug (prt "Running the rule for slot" slot-name))
(let result (it i)
(when debug (prt "...slot" slot-name "is" result))
result))
it)))

(mac defslot (name (o debug))
`(def ,name (i) (slot-value i ',name ,debug)))

(defslot on? t)

Same test tracing the on? slots

(loop (= outside*!temp 30)
(< outside*!temp 100)
(= outside*!temp (+ outside*!temp 10))
(prn)
(dumpworld)))

Looks OK, but watch what happens even if nothing is going on:

(dumpworld))

Ah, the downside of the functional paradigm: the code runs and runs. For simple functions that is no problem, but if we build an entire application this way things bog down (we learned the usual way).

What we need to cache the result of a calculation and then return the cached result when queried a second time. But then there is a new problem: when do we refresh the cache? Answer: when we have to in order to stay current with the changing world arounds us, more plainly when one of the values used in a calculation changes.

So we need to keep track of who uses whom in their calculations, and when one value changes notify its users that they need to recalculate.

Tomorrow.

No comments: