Baffled how to proceed...

159 views
Skip to first unread message

Jerry Jackson

unread,
Jun 19, 2017, 12:23:03 PM6/19/17
to Clara
Hello,

I'd like to use Clara for a translation, diffing, and merging task and I'm trying to use it in the way I expect a RETE engine to work. I've written a RETE engine in the past that was used in production so I thought I knew what to expect. However, it appears that Clara wants to do things very differently (perhaps because of the desire to provide truth maintenance). I'm stumped as to how to make sense of some of the behavior and I'll be grateful for any insight. First of all, this happens:

Given two files...

doit.clj:

(ns badnot.doit
  (:require [clara.rules :refer :all]
            [badnot.rules :as rules]))

(defn simple-query [session fun]
  (vec (map #(into {} (second (first (vec %)))) (query session fun))))

(defsession example 'badnot.rules
  :fact-type-fn :type)

(defn doit []
  (let [new-session (-> example
                        (insert-all [{:type :bar} {:type :bar}])
                        (fire-rules))]
    (doseq [val (simple-query new-session rules/get-foos)] (println val))))

and rules.clj:

(ns badnot.rules
  (:require [clara.rules :refer :all]
            [clara.rules.accumulators :as acc]))

(defrule foo
  "insert a foo if none exists"
  [:bar]
  [:not [:foo]]
  =>
  (insert-unconditional! {:type :foo}))

(defquery get-foos [] [?foo <- :foo])

executing (doit/doit):

user=> (require '[badnot.doit :as doit])
nil
user=> (doit/doit)
{:type :foo}
{:type :foo}
nil

I can't understand why I get two results here (BTW, the original case had a matching constraint between fields of "foo" and "bar" but I don't think that matters). The [:not] should prevent the second instance from firing after the first fires (or, at least that's what I expect and what the engine I wrote would have done). How should I arrange to make this rule only fire once? It seems like this is following the SOAR model of firing all eligible activations rather than just the highest priority before starting a new cycle. Is this true? Thanks for any help.

--Jerry Jackson

Jerry Jackson

unread,
Jun 20, 2017, 11:01:50 AM6/20/17
to Clara

Also, I don't understand how this rule:

(defrule one-service
  ""
  {:salience 2000}
  [?svc1 <- :service [{id :id}] (= ?id1 id)]
  [?svc2 <- :service [{id :id}] (= ?id2 id)]
  [:test (= ?id1 ?id2)]
  =>
  (retract! ?svc2))

can result in more than one service with the same id remaining in working memory. Why doesn't it continue to fire until there is only one left?

Again, thanks for any help.

William Parker

unread,
Jun 22, 2017, 11:26:19 AM6/22/17
to Clara
Clara batches activations in the same activation group together.  If you want to look at the source, I think the most relevant place would be fire-rules*.  I think what is happening here is that

- Both :foo facts get added the cache of insertions to execute.
- They are then executed at the same time.
- Since the :foo facts were inserted unconditionally the consequence of the rule is not retracted.

This sounds similar to https://github.com/cerner/clara-rules/issues/229 if you want to read through that.  I haven't looked at your second example yet; I'll do so and perhaps elaborate a bit more on this one when I get a chance.

William Parker

unread,
Jun 25, 2017, 7:44:46 PM6/25/17
to Clara
From looking at this rule I think we have a bug.  I created a more simplified example and logged issue 321.  I included links in the issue to some relevant parts of the code if you want to see why this is happening internally; feel free to seek clarification there or here if anything is unclear.

On this particular rule, from your expectation that you'd retract until only one service was present, I think you may have expected that Clara would prevent a fact from joining with itself, which is not the case.  This rule could fire with a single service, so I think the correct behavior would be for it to remove all service facts from the session.  If you want to normalize to a single "service view" per ID, you might consider using an accumulator or :exists condition (which is backed by an accumulator); see extract-exists in the compiler.  You could then insert a new fact of which there would be one per ID that you could use in downstream rules.  The accumulator doc discusses this some, specifically the paragraph after the first code block starting with "Note that we accumulate once per distinct set of bindings".  This wouldn't free the memory consumed by duplicate service facts if that is the behavior you need though.

Regarding your first example, IMO Clara's semantics around RHS retraction aren't as well-defined as they ideally should be, with the major complicating factor here being that the RHS retraction is interacting with rules in its own salience group, namely itself.  However, I see RHS retraction as a low-level operation that is sometimes useful but more often adds incidental complexity.  For the most part, our rulesets are deep DAG structures terminating in queries with everything after some initial layers that filter lots of data governed by truth maintenance.  This allows us to avoid the need to "think procedurally" when evaluating how a ruleset will behave.  Obviously use cases vary though, and low-level operations like RHS retractions and to a lesser extent unconditional RHS insertions can certainly be useful at times.

Thanks for the examples, concrete instances of behavior demonstrating a bug or exposing insufficiently well defined behavior are helpful in making Clara more robust.

Jerry Jackson

unread,
Jun 26, 2017, 6:56:07 AM6/26/17
to William Parker, Clara
Thanks very much for the response! I appreciate the in-depth explanation. The second rule joining with itself was, in fact, an oversight on my part when I was trying to work around my first problem. If it had actually removed all matching services, I believe I would have realized my mistake and fixed it. :-) Your answer confirms for me, I think, that the main goals of Clara are more specifically in the area of justified reasoning than in generic rule processing.  From other reading and experimentation I did, I think it's an excellent system that I would love to be able to use. Perhaps fully processing the results of one activation prior to selecting another one to fire could be a setting in mk-session? It looks like it would be a small change in rule firing.

My current application involves mapping between complex nested structures by reducing them to joined primitive elements and performing the mapping there before reconstruction. In this case, the ability to express a mapping without navigating the structures is the main functionality I'm looking for. I've come to see now that RETE-style rule engines really form a family of somewhat different beasts, each with its own focus and requirements. As yet another example, the engine we used for cluster management ran forever and sorted rule instantiations breadth-first by wmes after salience/priority to ensure that no member of the cluster was ignored for too long.

Nothing can be all things to all people. Good luck with Clara whether or not it ends up working for me!

Thanks again,
--Jerry Jackson


--
You received this message because you are subscribed to a topic in the Google Groups "Clara" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/clara-rules/iDecaQMSL9M/unsubscribe.
To unsubscribe from this group and all its topics, send an email to clara-rules+unsubscribe@googlegroups.com.
To post to this group, send email to clara...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/clara-rules/105b3fc8-4fa8-4c40-b5ec-74d28dffb6c0%40googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Reply all
Reply to author
Forward
0 new messages