Fork me on GitHub
#clara
<
2017-11-19
>
sparkofreason22:11:43

Sorry if this is documented somewhere, but I didn't catch it. Is there some best practice around handling fact identity for updates? For example, if have (defrecord Person [name address]), and I want to update the address, or perhaps even insert the same Person (by value) twice. Is there some way to tell clara to use name to identify the Person and change the replace the existing fact, or do I have to explicitly retract the existing instance? Or am I totally missing something for modeling this situation?

mikerod23:11:51

> do I have to explicitly retract the existing instance? Yes, the idea is to do this sort of change explicitly

mikerod23:11:00

and there is no concept of mutating a fact “in place”

mikerod23:11:16

There are several ways to approach the problem

mikerod23:11:20

@dave.dixon one idea is if you don’t actually care about deleting old data (perhaps you want to have it for historical rules etc), you could put an intermediary between external “person data at a point in time” vs the “current person” being processed:

(r/defrule current-person
  [?latest <- latest-person-acc :from [PersonSnapshot (= ?name name)]]
  =>
  (r/insert! (->Person ?latest)))

mikerod23:11:43

This is if you made some sort of latest-person-acc that chose the latest by some criteria of yours (like a PersonSnapshot timestamp)

mikerod23:11:56

You could use the clara.rules.accumulators/max for this

mikerod23:11:16

(r/defrule current-person
  [?latest <- (acc/max :timestamp) :from [PersonSnapshot (= ?name name)]]
  =>
  (r/insert! (->Person ?latest)))

mikerod23:11:20

You may not want to leave facts in though due to a concern, like memory use or something. Then a common way to deal with the update is somewhat external to the rules processing themselves

mikerod23:11:37

(defrecord Person [name x])

(r/defquery person-with-name [:?name]
  [?p <- Person (= ?name name)])

(def init
  (-> (r/mk-session [person-with-name])
      (r/insert (->Person "joe" 10)
                (->Person "mary" 20))
      r/fire-rules))

(defn find-person-by-name [session pname]
  ;; Assumes name is unique (you could do something different)
  (-> session
      (r/query person-with-name :?name pname)
      first
      :?p))

(defn update-person [session new-person]
  (let [existing (find-person-by-name session (:name new-person))]
    (-> session
        (r/retract existing)
        (r/insert new-person)
        r/fire-rules)))

(def updated
  (update-person init (->Person "joe" 50)))



;; Then try:

(find-person-by-name init "joe")
(find-person-by-name updated "joe")

mikerod23:11:04

That sort of pattern would allow you take rm and old version of the person and insert an update

mikerod23:11:37

and it avoids getting into situations within the engine where you have to deal with logical loops in the truth maintenance system and/or unconditional inserts, RHS retracts, etc

mikerod23:11:05

You could impl the update-person differently where you just make a change to the existing fact to get some data structural sharing too though instead of a brand new fact like I had

mikerod23:11:41

e.g.

(defn update-person [session new-person-data]
  (let [existing (find-person-by-name session (:name new-person-data))
        up (merge existing new-person-data)]
    (-> session
        (r/retract existing)
        (r/insert up)
        r/fire-rules)))

;; e.g.
(update-person init {:name "joe" :x 50})