Fork me on GitHub

oh right it isn't terminating


in that case I'd write a rule that should match on ANY Operation, and have a println on the RHS


that way it should activate if an Operation is ever inserted


@olivergeorge This is rough, but hopefully correct (thought about it quickly) So I think the loop goes like this (only including relevant fields): 1) insert Field{:field-key :site :read-only true} 2) activate rule1 3) fire rule1 - insert FieldState{:field-key :site :read-only true} 4) activate rule2 5) fire rule2 - insert Operation{:field-key :site} 6) retract previous rule1 results since (acc/all) changed 7) retract previous rule2 results since rule1 retracted the matching fact changed ;; due to (6) 8) retract previous rule1 results since (acc/all) changed ;; due to (7) - no actual retraction needed here, just updates the accumulator again (6) already retracted 9) (re-)activate rule1 ;; due to (8) 10) insert Field{:field-key :site :read-only true} ;; no Op1 anymore 11) repeat (4) and (5) 12) repeat (6) (7) ( 8 ) (9) 13) keep repeating (11) (12)

Oliver George00:09:10

Ah! right. That makes sense. Thank you.

Oliver George00:09:28

I missed the op retraction.


so you have to do something to avoid the cycle that starts there


perhaps checking some field that would stop the “feedback loop”


but it’d be domain dependent I think to know the answer in that particular case

Oliver George00:09:17

I wondered about having a fire-rules loop and doing (field+ops => field-state) aggregation between iterations.

Oliver George00:09:03

I can see other possibilities.

Oliver George00:09:08

Thanks for putting me on track


@mikerod do you know of any solid references for designing forward inference rule systems? Or some common design patterns, etc?


@dadair good question. I’m don’t really think I know a lot off the top of my head. Which is unfortunate


I’ll have to think about that one

Oliver George01:09:56

Is there a simple way to extract all facts from a session?

Oliver George01:09:21

My best guess is query with an expression whose fact type is an ancestor of all records which seems to work just fine for records using [?ret <- Object].

Oliver George02:09:24

@dadairI was going to suggest that perhaps the Doorenbos paper might be worth checking for references but it's 150 pages long.

Oliver George04:09:54

One technique which might have resolved my problem is using insert-unconditional! for Operations as that would ensure they aren't retracted (no feedback loop).

Oliver George04:09:19

Not sure if using insert-unconditional! is considered a code smell / something to be avoided rather than common usage.

Oliver George04:09:31

(ns spike-rules.round1
  (:require [clara.rules :as rules :refer [insert! insert-unconditional! defrule defquery defsession]]
            [clara.rules.accumulators :as acc]))

(defrecord Field [field-key field-state])
(defrecord FieldState [field-key field-state])
(defrecord Disabled [field-key])

(defn exists
     {:initial-value     false
      :reduce-fn         (constantly true)})))

(defrule rule1
  "Insert FieldState based on Field and Operations"
  [?field <- Field (= field-key ?field-key)]
  [?disabled <- (exists) :from [Disabled (= field-key ?field-key)]]
    (cond-> (->FieldState ?field-key (:field-state ?field))
      ?disabled (assoc :disabled true))))

(defrule rule2
  "disable if read-only"
  [FieldState (= ?field-key field-key) (:read-only field-state)]
  [:not [Disabled (= ?field-key field-key)]]
  (insert-unconditional! (->Disabled ?field-key)))

(defquery export-facts [] [?ret <- Object])

(defsession form-session [rule1 rule2 export-facts])

(-> form-session
      (->Field :site {:value nil :read-only true}))
    (rules/query export-facts))


> I was going to suggest that perhaps the Doorenbos paper might be worth checking for references but it’s 150 pages long. @olivergeorge I do think that this paper is a great explanation of Rete (skipping some of the out-dated about some of the “testbed” system parts). I’m not sure it has a lot on “how to structure and design rules in practice” though.


When looking for that sort of guidance I think I’ve sort of just hunted around the internet searching for like “rules engines best practices”


I think Drools (popular Java/JVM based rules engine) had a fairly large amount of material with some general guidance on things. It’s still a search though. I wish I knew more direct sources though.


@olivergeorge I’d be careful using insert-unconditional!


You start to get into an order dependent situation with your rules


if an “upstream” fact from a different rule is logically inserted and later retracted, but a downstream insert-unconditional! has already happened, it won’t be retracted and you’ll get into an inconsistent state


So then you end up having to do things like add :salience to try to force the rule to fire later than another and this ends up just becoming increasingly brittle


Also, loses the “declarative” sort of property that you get with rules all being under control of the truth maintenance system


Organizing rules sets - My rule set has become reasonably large within a short time. I keep getting issues with Exceptions against the schema. These are really difficult to resolve. I wonder if this is because I am running in a repl rather than outside? However, I thought I would divide my project into 4 namespaces - rules (for those I know work) worker (for any functions that help with processing my data into the working memory) queries . This then leave me with an issue about what to do with the record definitions that need to be available in all of these and in the core where I am creating the session. I either get cyclical dependencies if I put the record defs in any of these files - so I move them out into another records namespace and each of the above and the core require and refer this namespace. However when I run the mk->session if fails to find the record defs. Any ideas?


“However when I run the mk->session if fails to find the record defs. ” It sounds like the namespace defining the records wasn’t compiled then. In case you don’t know, Clojure won’t actually compile a namespace until something requires it. It can be weird in that if you have a fully qualified reference, it will resolve even if the namespace in question doesn’t require the namespace, but only if something required the namespace already. I personally think of Clojure’s compiler as a “procedural” one that takes in code and alters the state of the runtime environment accordingly.


I have a shared namespace for shared records, then I build sessions like so: (r/mk-session 'rules.shared 'rules.x 'rules.y)


you need to make sure you are requiring the namespace too before making the session, I've run into that before


so either put rules.shared into the (ns .. (:require ..)) or do an explicit (require 'rules.shared)


Thanks - what if you have helper functions that need the shared? Do you lead them into the session as well? i.e. if a rule uses a function (private-ref? ?value) its namespace have to be made available to the session?


Thanks for the reply, BTW?


shared functions can live in the shared namespace


then in rules.x you can require rules.shared so you can use the functions


I only call (mk-session 'rules.shared 'rules.x ..) in a "core" namespace


so for example, rules.x, rules.y, and rules.z can require and use records/functions from rules.shared, and then in some other namespace rules.engine, you can require all the namespaces and call the mk-session function: (r/mk-session 'rules.shared 'rules.x ..)


that's how I've avoided circular dependencies


Thanks - re-organised the project as you said. I am still getting the issue where the first record typr encountered by the rules engine in the session is saying that the Record type can't be found. This namespace stuff is so hard to handle with the level of error messages from clojure

Oliver George22:09:59

Thanks for the tips @mikerod


depends how you are using Operation


can you post an example rule?


or the exact line of the error?