oracle-samples/clara-rules

Firing rules ends up in a loop

Closed this issue · 4 comments

sdaro commented

Hello,
I have a problem with clara rules and I hope you can help me.
To resolve components that are productive I create facts of dependencies and mark components that are used by productive components as productive as well.
Right when I have a loop in the dependency chain, clara rules ends up in a endless loop while firing rules.
Here is the code:

(ns clara.error
  (:require [clara.rules :refer :all]))

(defrecord DependsOn [element dep])
(defrecord Productive [element])

(defrule productive
  ""
  [DependsOn (= ?element1 element) (= ?element2 dep)]
  [Productive (= ?element1 element)]
  =>
  (insert! (->Productive ?element2)))

; Works
(as-> (mk-session 'clara.error) $
      (apply insert $ [(->Productive "A")])
      (apply insert $ [(->DependsOn "A" "B")
                       (->DependsOn "B" "C")
                       (->DependsOn "C" "D")])
      (fire-rules $))

; ends in a loop
(as-> (mk-session 'clara.error) $
      (apply insert $ [(->Productive "A")])
      (apply insert $ [(->DependsOn "A" "B")
                       (->DependsOn "B" "C")
                       (->DependsOn "C" "A")])
      (fire-rules $))

The approach of clara rules is awesome. That's why I hope we can find the problem so I can continue using clara rules.
Thanks in advance.

Hi @sdaro,

As you said, when there is a cycle in the data being added clara will run into an infinite loop. This is due to clara trying to find a steady state, ie. no new facts inserted into the session. Meaning that in the scenario above there will always be a new fact insert((->Productive "A")).

I'm not fully aware of your use-cases but perhaps a few changes like:

(ns user
   (:require [clara.rules :as r]
            [clara.rules.accumulators]))


(defrecord DependsOn [element dep])
(defrecord Productive [element])
(defrecord ProductiveInternal [element path])

(defn vec-contains?
  [v item]
  (some #(= % item) v))

(r/defrule map-internal-productive
  [Productive (= ?element element)]
  =>
  (r/insert! (->ProductiveInternal ?element [])))

(r/defrule join-rule
  [DependsOn (= ?element element) (= ?dep dep)]
  [ProductiveInternal (= ?element element)
   (= ?path path)]
  [:test (not (vec-contains? ?path ?element))]
  =>
  (r/insert! (->ProductiveInternal ?dep (conj ?path ?element))))

(r/defquery cycle?
  []
  [:exists [ProductiveInternal (vec-contains? path element)]])

(defn run []
  (-> (r/mk-session)
      (r/insert-all [(->Productive "A")
                     (->DependsOn "A" "B")
                     (->DependsOn "B" "C")

                     (->DependsOn "C" "A")])
      (r/fire-rules)
      (r/query cycle?)))

In the rules above i'm preventing the cycle from producing an infinite loop with the new guard and field:

[:test (not (vec-contains? ?path ?element))]

and in the event of a cycle the query should return a single result:

(run)
=>
({})

otherwise it should be an empty seq.

Agreed with @EthanEChristian 's assessment above - Clara will continue firing rules, including using newly inserted facts, until nothing is left to be fired and a logical final state is reached. http://www.clara-rules.org/docs/truthmaint/ might be good to read. This is analogous to infinite recursion causing a stack overflow error. Does this make sense?

sdaro commented

@EthanEChristian Thank you! That was the missing piece of the puzzle. This solves my problem, finally.

@WilliamParker Thanks for the reference and the explanation. I will look into it.

@sdaro,
I'm going to close this issue.