Update of /project/cells/cvsroot/kennysarc2 In directory clnet:/tmp/cvs-serv15889
Added Files: cells-1.arc Log Message: Trying to get warning-free build
--- /project/cells/cvsroot/kennysarc2/cells-1.arc 2008/02/14 12:30:22 NONE +++ /project/cells/cvsroot/kennysarc2/cells-1.arc 2008/02/14 12:30:22 1.1 ;;; Utilities ;;; ---------
(def prt args (apply prs args) (prn))
(mac prun (banner . forms) `(do (prn ,banner) (prn '(do ,@forms)) (prn) ,@forms))
(prun " Cells ----- Let's start with the moral equivalent of a C++ member function, or at least I think that's what they call it. I mean what looks like a stored data member or slot of an attribute of an object that 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 boiler. "
(= b* (obj outside-temp 72 on? [< _!outside-temp 50])))
(prun " 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)))
(prun "
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)))
(prun "
-> 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)))
(prun " Test new slot reader, setting temp high enough this time so that the boiler should go off: " (= b*!outside-temp 80) (pr-boiler b*))
(prn "
Super. 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)))
(prun " Can a slot of a different boiler be maintained by other code? Start with a hard-coded NIL for on?... "
(= b* (obj outside-temp -10 on? nil)) (pr-boiler b*))
(prun " Now assign t to on? " (= b*!on? t) ; We'll hide the assignment implementation later. (pr-boiler b*))
(prun " Super.
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)))
(prun " And test: " (= b* (obj outside-temp 20 on? nil inside-temp [if (on? _) 72 _!outside-temp])) (pr-boiler b*))
(prun " Super. Now let's bring back the automatic boiler: " (= b*!on? [< _!outside-temp 50]))
(prun " Step 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*)))
;;; Super. But 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*)))
;;;(prun " ;;;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)) ;; i is like self ala Smalltalk (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)
;;;(prun " ;;;Same test tracing the on? slots ;;;" ;;; (loop (= outside*!temp 30) (< outside*!temp 100) (= outside*!temp (+ outside*!temp 10)) ;;; (prn) ;;; (dumpworld)))
(prun " 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 do is cache a calculation and then return the cached ;;; result when queried a second time. But then when do we refresh the ;;; cache? Answer: when we have to to stay current with the changing ;;; world arounds us, more prosaically when one of the values used to ;;; calculate the current cache value has changed. ;;; ;;; 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. ;;; ;;; Next time.