clara

http://www.clara-rules.org/
Matthew Pettis 2020-09-17T02:39:50.016900Z

Is there a way to import defrec definitions from another namespace to use in a rules session? I want to modularize my rules, queries, and record definitions into different namespaces, and am having troubles getting the definitons/objects recognized in a rules session. Below is a the most stripped-down example I could make that I cannot figure out...

Matthew Pettis 2020-09-17T02:39:58.017100Z

user=> (ns a
  #_=>   "Has the record definitions")
nil
a=> (defrecord MyRec [nm])
a.MyRec


a=> (ns b
#_=>     "Has the logic to run the clara rules session"
#_=>     (:require
#_=>         [clara.rules :as r]
#_=>         [a :as nsa])
#_=>     (:import (a MyRec))
#_=> )
nil


b=> (r/defrule therec
#_=>   "Has a MyRec record"
#_=>   [?r <- nsa/MyRec]
#_=>   =>
#_=>   (r/insert! (nsa/->MyRec :output)))
#'b/therec


b=> (r/defquery qrec
#_=>   []
#_=>   [?e <- nsa/MyRec])
#'b/qrec


b=> (def sess (r/mk-session))
Syntax error (ClassNotFoundException) compiling at (form-init1616251358278573848.clj:1:11).
MyRec

ethanc 2020-09-17T02:47:32.018600Z

@matthew.pettis, i believe that you would want to remove the alias qualifier from the conditions of the query and the rule. nsa/MyRec would instead be MyRec

Matthew Pettis 2020-09-17T02:49:47.018800Z

user=> (ns a
  #_=>   "Has the record definitions")
nil
a=> (defrecord MyRec [nm])
a.MyRec


a=> (ns b
#_=>     "Has the logic to run the clara rules session"
#_=>     (:require
#_=>         [clara.rules :as r]
#_=>         [a :as nsa])
#_=>     (:import (a MyRec))
#_=> )
nil


b=> (r/defrule therec
#_=>   "Has a MyRec record"
#_=>   [?r <- MyRec]
#_=>   =>
#_=>   (r/insert! (->MyRec :output)))
#'b/therec


b=> (r/defquery qrec
#_=>   []
#_=>   [?e <- MyRec])
#'b/qrec


b=> (def sess (r/mk-session))
Syntax error compiling at (form-init2860030196736207126.clj:1:11).
Unable to resolve symbol: ->MyRec in this context

Matthew Pettis 2020-09-17T02:50:48.019800Z

Different error message though... I keep trying permutations and am having a hard time tracking what each means...

ethanc 2020-09-17T02:51:43.020700Z

you would still want to qualify the position based constructor function in the RHS of the rule

Matthew Pettis 2020-09-17T02:54:49.021700Z

ah, right, the difference makes sense. That fixed the error messages, thanks! But I'm getting an empty query result, but I expect to find two facts with the query... any advice there?

Matthew Pettis 2020-09-17T02:54:53.021900Z

b=> (ns a
#_=>   "Has the record definitions")
nil
a=> (defrecord MyRec [nm])
a.MyRec


a=> (ns b
#_=>     "Has the logic to run the clara rules session"
#_=>     (:require
#_=>         [clara.rules :as r]
#_=>         [a :as nsa])
#_=>     (:import (a MyRec))
#_=> )
nil


b=> (r/defrule therec
#_=>   "Has a MyRec record"
#_=>   [?r <- MyRec]
#_=>   =>
#_=>   (r/insert! (nsa/->MyRec :output)))
#'b/therec


b=> (r/defquery qrec
#_=>   []
#_=>   [?e <- MyRec])
#'b/qrec


b=> (def sess (r/mk-session))
#'b/sess


b=> (let [qsess (-> sess
#_=>                (r/insert-all (nsa/->MyRec :init))
#_=>                r/fire-rules
#_=>                (r/query qrec))]
#_=>   qsess)
()

Matthew Pettis 2020-09-17T02:58:34.023100Z

(But I see that that is not related to my namespace issue... I'm doing something wrong with just basic rules, I think...)

ethanc 2020-09-17T02:58:44.023400Z

insert-all in this case is assuming that you passed a list and is probably breaking the record down into a list of keyvalue pairs

ethanc 2020-09-17T02:59:05.023600Z

(r/insert-all [(nsa/->MyRec :init)])

ethanc 2020-09-17T02:59:50.024600Z

however this will cause an infinite loop do to therec inserting a fact that then triggers itself

ethanc 2020-09-17T02:59:56.024900Z

*due

Matthew Pettis 2020-09-17T03:00:08.025200Z

ach, yeah, i copied that over from a place where I was inserting a list, thanks for the catch!

Matthew Pettis 2020-09-17T03:08:41.025800Z

For posterity, here is a working example where I'm importing the defrec from another namespace... And thank you @ethanc for the great help!

1❤️
Matthew Pettis 2020-09-17T03:08:46.026Z

user=> (ns a
  #_=>   "Has the record definitions")
nil
a=> (defrecord MyRec [nm])
a.MyRec


a=> (ns b
#_=>     "Has the logic to run the clara rules session"
#_=>     (:require
#_=>         [clara.rules :as r]
#_=>         [a :as nsa])
#_=>     (:import (a MyRec))
#_=> )
nil


b=> (r/defrule therec
#_=>   "Has a MyRec record"
#_=>   [?r <- MyRec (= :nm :init)]
#_=>   =>
#_=>   (r/insert! (nsa/->MyRec :output)))
#'b/therec


b=> (r/defquery qrec
#_=>   []
#_=>   [?e <- MyRec])
#'b/qrec


b=> (def sess (r/mk-session))
#'b/sess


b=> (let [qsess (-> sess
#_=>                (r/insert-all [(nsa/->MyRec :init)])
#_=>                r/fire-rules
#_=>                (r/query qrec))]
#_=>   qsess)
({:?e #a.MyRec{:nm :init}})

Matthew Pettis 2020-09-17T03:24:53.028400Z

Ok, so I tried splitting defrec, rules, and queries up into their own namespace, and run a session in a namespace separate from those three. With the help above, I was able to not get errors. But in the final query, I expect to see a single Outp record, but the query gives me (). It should be a simple step from what I did above, but I'm not seeing where I went wrong... UPDATE: Below, my rules LHS tried to match the record field as a symbol, not as a symbol. Fixing below, see update further down.

Matthew Pettis 2020-09-17T03:24:58.028600Z

rules-engine.core=> (ns rules-engine.rec-defs
               #_=>   "Records to use in rules")
nil

rules-engine.rec-defs=> (defrecord Inp [nm])
rules_engine.rec_defs.Inp
rules-engine.rec-defs=> (defrecord Outp [nm])
rules_engine.rec_defs.Outp



rules-engine.rec-defs=> (ns rules-engine.rules
                   #_=>   "Rules to import"
                   #_=>   (:require [clara.rules :as r]
                   #_=>             [rules-engine.rec-defs :as rd])
                   #_=>   (:import (rules_engine.rec_defs Inp Outp))
                   #_=>   )
nil


rules-engine.rules=> (r/defrule has-inp
                #_=>   "Has a Inp record"
                #_=>   [?r <- Inp (= :nm :init)]
                #_=>   =>
                #_=>   (r/insert! (rd/->Outp :output)))
#'rules-engine.rules/has-inp



rules-engine.rules=> (ns rules-engine.queries
                #_=>   "Queries"
                #_=>   (:require [clara.rules :as r]
                #_=>             [rules-engine.rec-defs :as rd])
                #_=>   (:import (rules_engine.rec_defs Inp Outp)))
nil

rules-engine.queries=> (r/defquery all-outp
                  #_=>   []
                  #_=>   [?e <- Outp])
#'rules-engine.queries/all-outp



rules-engine.queries=> (ns rules-engine.run-it
                  #_=>   "Main script to run a rules set"
                  #_=>   (:require [clara.rules :as r]
                  #_=>             [rules-engine.rec-defs :as rd]
                  #_=>             [rules-engine.rules :as rl]
                  #_=>             [rules-engine.queries :as q]
                  #_=>             )
                  #_=>   (:import (rules_engine.rec_defs Inp Outp))
                  #_=>   )
nil


rules-engine.run-it=> (def sess (r/mk-session 'rules-engine.rules 'rules-engine.queries))
#'rules-engine.run-it/sess

rules-engine.run-it=> (let [qsess (-> sess
                 #_=>                (r/insert-all [(rd/->Inp :init)])
                 #_=>                r/fire-rules
                 #_=>                (r/query q/all-outp))]
                 #_=>   qsess)
()

Matthew Pettis 2020-09-17T03:43:17.029300Z

Well, I know it is my logic, not the namespace stuff... this version works, I'll work on the one above...

Matthew Pettis 2020-09-17T03:43:22.029500Z

rules-engine.run-it=> (ns rules-engine.rec-defs
                 #_=>   "Records to use in rules")
nil
rules-engine.rec-defs=>

rules-engine.rec-defs=> (defrecord Myrec [nm])
rules_engine.rec_defs.Myrec



rules-engine.rec-defs=> (ns rules-engine.rules
                   #_=>   "Rules to import"
                   #_=>   (:require [clara.rules :as r]
                   #_=>             [rules-engine.rec-defs :as rd])
                   #_=>   (:import (rules_engine.rec_defs Myrec)))
nil

rules-engine.rules=> (r/defrule has-inp
                #_=>   "Has a Inp record"
                #_=>   [?r <- Myrec (= :nm :init)]
                #_=>   =>
                #_=>   (r/insert! (rd/->Myrec :output)))
#'rules-engine.rules/has-inp



rules-engine.rules=> (ns rules-engine.queries
                #_=>   "Queries"
                #_=>   (:require [clara.rules :as r]
                #_=>             [rules-engine.rec-defs :as rd])
                #_=>   (:import (rules_engine.rec_defs Myrec)))
nil

rules-engine.queries=> (r/defquery all-outp
                  #_=>   []
                  #_=>   [?e <- Myrec])
#'rules-engine.queries/all-outp

rules-engine.queries=> (ns rules-engine.run-it
                  #_=>   "Main script to run a rules set"
                  #_=>   (:require [clara.rules :as r]
                  #_=>             [rules-engine.rec-defs :as rd]
                  #_=>             [rules-engine.rules :as rl]
                  #_=>             [rules-engine.queries :as q]
                  #_=>             )
                  #_=>   (:import (rules_engine.rec_defs Myrec)))
nil

rules-engine.run-it=> (def sess (r/mk-session 'rules-engine.rules 'rules-engine.queries))
#'rules-engine.run-it/sess

rules-engine.run-it=> (let [qsess (-> sess
                 #_=>                (r/insert-all [(rd/->Myrec :init)])
                 #_=>                r/fire-rules
                 #_=>                (r/query q/all-outp))]
                 #_=>   qsess)
({:?e #rules_engine.rec_defs.Myrec{:nm :init}})

Matthew Pettis 2020-09-17T03:56:11.029900Z

Fixed session from above:

Matthew Pettis 2020-09-17T03:56:16.030100Z

rules-engine.run-it=> (ns rules-engine.rec-defs
                 #_=>   "Records to use in rules")
nil

rules-engine.rec-defs=> (defrecord Inp [nm])
rules_engine.rec_defs.Inp
rules-engine.rec-defs=> (defrecord Outp [nm])
rules_engine.rec_defs.Outp
rules-engine.rec-defs=>



rules-engine.rec-defs=> (ns rules-engine.rules
                   #_=>   "Rules to import"
                   #_=>   (:require [clara.rules :as r]
                   #_=>             [rules-engine.rec-defs :as rd])
                   #_=>   (:import (rules_engine.rec_defs Inp Outp))
                   #_=>   )
nil

rules-engine.rules=> (r/defrule has-inp
                #_=>   "Has a Inp record"
                #_=>   [?r <- Inp (= nm :init)]
                #_=>   =>
                #_=>   (r/insert! (rd/->Outp :output)))
#'rules-engine.rules/has-inp



rules-engine.rules=> (ns rules-engine.queries
                #_=>   "Queries"
                #_=>   (:require [clara.rules :as r]
                #_=>             [rules-engine.rec-defs :as rd])
                #_=>   (:import (rules_engine.rec_defs Inp Outp)))
nil

rules-engine.queries=> (r/defquery all-outp
                  #_=>   []
                  #_=>   [?e <- Outp])
#'rules-engine.queries/all-outp

rules-engine.queries=> (r/defquery all-inp
                  #_=>   []
                  #_=>   [?e <- Inp])
#'rules-engine.queries/all-inp



rules-engine.queries=> (ns rules-engine.run-it
                  #_=>   "Main script to run a rules set"
                  #_=>   (:require [clara.rules :as r]
                  #_=>             [rules-engine.rec-defs :as rd]
                  #_=>             [rules-engine.rules :as rl]
                  #_=>             [rules-engine.queries :as q]
                  #_=>             )
                  #_=>   (:import (rules_engine.rec_defs Inp Outp)))
nil

rules-engine.run-it=> (def sess (r/mk-session 'rules-engine.rules 'rules-engine.queries))
#'rules-engine.run-it/sess

rules-engine.run-it=> (let [qsess (-> sess
                 #_=>                (r/insert-all [(rd/->Inp :init)])
                 #_=>                r/fire-rules
                 #_=>                (r/query q/all-outp))]
                 #_=>   qsess)
({:?e #rules_engine.rec_defs.Outp{:nm :output}})

2020-09-17T17:38:08.030600Z

@matthew.pettis it’s just clj rules here for how you refer to record types

2020-09-17T17:38:24.031100Z

in cljs you use ns’s and aliases like your stuff above

2020-09-17T17:38:31.031400Z

in clj (unfortunately), you have to use interop

2020-09-17T17:39:02.032100Z

so the actual :import class symbols

2020-09-17T17:39:06.032300Z

nothing from a :require + :as etc

Matthew Pettis 2020-09-17T17:41:52.034700Z

@mikerod Thanks. Yep, I hit this problem once before (I think I posted it here too)... The way I am currently thinking about it is: for rules that do matching on the LHS, I need to import the classes with :import. When I want to construct a record I need to insert on the RHS, with ->Myrecord, I need to require the namespace from which I have my defrecord declaration so I have that constructor. Does that seem right?

2020-09-17T17:52:29.035200Z

@matthew.pettis I do not like it. I think when writing non-interop clj, things like :import should not be necessary at all

2020-09-17T17:52:41.035700Z

I don’t like that Clara basically enforces this needing to be done for records

2020-09-17T17:53:02.036200Z

Clj does offer some “factory functions” it auto-creates with defrecord - which allows you to not have to use interop forms to create records

2020-09-17T17:53:20.036700Z

What clj does not offer, is how to refer to records in a way without interop (eg. :import)

2020-09-17T17:53:34.037100Z

More frustrating to me, is CLJS does allow you to refer to record types via :require + :as

2020-09-17T17:54:05.037800Z

so this causes lots of additional problems with say , making cljc files where you want to use :as aliases, but then records need to be specified different ways for :clj vs :cljs

2020-09-17T17:54:53.038600Z

so that’s some background. I don’t know what exactly Clara should/could do here to help. However, Clara’s compilation has control over symbol resolution so it’s likely something could be done to allow for no :import to be needed

2020-09-17T17:55:14.039Z

I think probably, Clara could attempt to support the cljs style record syntax

2020-09-17T17:55:23.039400Z

like a/MyRec

2020-09-17T17:55:52.040200Z

and just implicitly know to resolve it in clj following some rules like: 1) first see if a/MyRec exists 2) if not, try a.MyRec1

2020-09-17T17:56:40.041300Z

this could be convenient for Clara’s perspective. it would deviate from the perspective of what clj actually allows

2020-09-17T17:56:44.041500Z

so there’s a pro vs con

Matthew Pettis 2020-09-17T18:03:14.045500Z

That would be nice to not have the interop require vs. import split, as it seems a typical rules pattern is to have record class referenced in the LHS, and needs to construct and insert a record on the RHS, which needs the constructor via require. It took me a while to suss out the difference, and a post by Alex Miller (which I can't find today) helped me understand it a bit more. I considered going down to the plain maps workflow and not using records, which would take care of this issue, and I assume people who don't use records make code that should mostly run in either CLJ or CLJS.

Matthew Pettis 2020-09-17T18:06:13.048400Z

But I'll say that Clara still seems to have better semantics and usability than other solutions I've tried. durable_rules from jruizgit at github works, but not for my use case, where I like the concept of queries that can return me the facts in the session, which durable_rules does not in theory (jruiz made a function to do so after a an issue I filed, but it appears to still have some bugs).

2020-09-17T18:06:40.048700Z

Nice that Clara has worked better for you!

2020-09-17T18:06:53.049Z

and I really wish clj would have supported non-interop ways to refer to records

2020-09-17T18:06:56.049200Z

it’s much nicer in cljs

2020-09-17T18:07:27.049900Z

you will see in clara-rules test ns’s we have a lot of cljc stuff. And you’ll see a lot of read-conditionals around the difference between having to use :require in cljs for records, vs :import for clj

2020-09-17T18:07:54.050400Z

but yeah, feel free to log a clara-rules github issue if you want to propose allowing the syntactic forms perhaps

2020-09-17T18:08:05.050900Z

the only thing I don’t like as much as it differs from how clj resolves symbols

2020-09-17T18:08:18.051500Z

but we do that in other ways throughout forms as well - so maybe not that big of a deal

2020-09-17T18:08:33.052Z

and records can be quite convenient over maps (although both are supported)

2020-09-17T18:08:50.052800Z

records have another benefit in that clara compiler will “understand field references”

2020-09-17T18:09:03.053400Z

so you can do like [MyRec (= ?x x)] where x is a field on MyRec

2020-09-17T18:09:47.054800Z

if you used a map, you’d have to do [:my-rec (= ?x (;x this))] (assuming your map used the clara default type impl to produce that :my-rec type)

Matthew Pettis 2020-09-17T18:10:25.055800Z

in disclosure, I am not so much a programmer as a data scientist with some ability to code, and I have a some applications I can really see rules engine bridging for me, so I am learning Clojure and Clara mostly at the same time. I say that so when you see crazy recommendations from me, you'll take it with a grain of salt.

2020-09-17T18:10:27.055900Z

and lastly, records can have some efficiency gains where some cases that may matter

Matthew Pettis 2020-09-17T18:12:40.057800Z

yeah, that's why I like the records approach -- it seemed like a much more comprehensible syntax to make rules with. I haven't had to address performance yet, as my rulesets are not that large, and probably won't be for what I am envisioning.

1👍