oracle-samples/clara-rules

Salience/ordering required to determine reasoning?

Closed this issue · 6 comments

The following prints "allowed" followed by "denied". "denied" should not be printed after the first rule fired. I can get the second rule not to fire but only if I put (->Decision :allow) of first rule in the initial mk-session.

(defrecord Decision [status])

(defrule has-email-allowed
[User (= "wravel@mail.com" email)] ; some true statement
=>
(insert! (->Decision :allow))
(println "allowed"))

(defrule has-email-deny
[:not [Decision (= status :allow)]]
=>
(insert! (->Decision :deny))
(println "denied"))

I did find a workaround, making the salience higher on first remedies it, but should that really be needed?

@jtrunick,
I was also able to replicate the scenario above, however if you add a query to the productions that you already have, ie.

(defquery decision-query
  []
  [?dec <- Decision])

and run the sessions as usual:

(-> (mk-session)
    (insert (->User "wravel@mail.com"))
    fire-rules
    (query decision-query))
allowed
denied
=> ({:?dec #user.Decision{:status :allow}})

The end state of the session is as expected, with a single Decision in an allowed status.

As alluded to in your workaround, you can manipulate this behavior by changing the activation groups of the rules above such that the has-email-allowed has an earlier activation group than has-email-deny, ex.

(defrule has-email-allowed
  {:activation-group ::tier-1}
  ...)

(defrule has-email-deny
  {:activation-group ::tier-2}
  ...)

(-> (mk-session
      :cache false
      :activation-group-fn (fn [production]
                                       (or (some->> production :props :activation-group (get {::tier-1 -1 ::tier-2 1}))
                                           0))
      :activation-group-sort-fn (fn [y x]
                                            (cond
                                              (= x y) 0
                                              (> x y) -1
                                              :else 1)))
    (insert (->User "wravel@mail.com"))
    fire-rules
    (query decision-query))

allowed
=> ({:?dec #user.Decision{:status :allow}})

To answer,

but should that really be needed?

Currently(and likely in the future), yes. My reasoning here is that from a rule developer's prospective I can easily determine that the has-email-allowed should be evaluated for truthiness prior to has-email-deny. However as clara allows arbitrary clojure in the right hand side it would not be easy(possible?) to determine what facts are inserted, such that clara could reasonably make the activation group dynamically.

@jtrunick @EthanEChristian

I want to add a note on "good practice".

It is generally best if you can keep your rules free of side effects that the rule engine is not aware of. This means any effect other than manipulation of working memory - via r/insert!, r/retract!, and other similar.

If you do an effect, such as println in this example, you become reliant on the order of execution (aka "firing" order) of rules. In general, you want to emphasize writing rules that are not sensitive to the order in which rules are fired.

Clara defaults with r/insert! to be a "logical insertion". This uses a truth maintenance system (aka TMS). Part of the rationale for this is to facilitate order independence on working memory updates. The result is that rules RHS may be "fired"/evaluated multiple times as working memory state is changing. Eventually, the working memory stops being changed, and what you are left with is a working memory state of facts that are logically consistent with the LHS logic of the rules.

In most cases, I wouldn't try to solve ordering problems with :salience or :activation-group unless there were specific compelling reasons to do so (perhaps performance suffers from poorly ordered rule firing and there are robust heuristics that you can express via these ordering properties).

It's often best to keep the rules as pure as possible, if not completely pure. Have an external process interpret the working memory state results - via queries - to perform the actual external side effects - in this basic example, that means the println.

OK, I understand the side effect/print issue. Thanks for pointing that out.

Also, I was confused, but it looks like Clara is handling this fine w/o salience, it's retracting the match info as well from the :rule-matches. That's great.

@jtrunick is there anything left to discuss here?

Closing this issue, if there are further questions we can log a new issue.