Fork me on GitHub
#clara
<
2020-09-17
>
Matthew Pettis02:09:50

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 Pettis02:09:58

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

ethanc02:09:32

@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 Pettis02:09:47

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 Pettis02:09:48

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

ethanc02:09:43

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

Matthew Pettis02:09:49

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 Pettis02:09:53

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 Pettis02:09:34

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

ethanc02:09:44

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

ethanc02:09:05

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

ethanc02:09:50

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

Matthew Pettis03:09:08

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

Matthew Pettis03:09:41

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

❤️ 3
Matthew Pettis03:09:46

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 Pettis03:09:53

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 Pettis03:09:58

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 Pettis03:09:17

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

Matthew Pettis03:09:22

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 Pettis03:09:11

Fixed session from above:

Matthew Pettis03:09:16

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}})

mikerod17:09:08

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

mikerod17:09:24

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

mikerod17:09:31

in clj (unfortunately), you have to use interop

mikerod17:09:02

so the actual :import class symbols

mikerod17:09:06

nothing from a :require + :as etc

Matthew Pettis17:09:52

@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?

mikerod17:09:29

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

mikerod17:09:41

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

mikerod17:09:02

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

mikerod17:09:20

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

mikerod17:09:34

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

mikerod17:09:05

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

mikerod17:09:53

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

mikerod17:09:14

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

mikerod17:09:23

like a/MyRec

mikerod17:09:52

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

mikerod17:09:40

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

mikerod17:09:44

so there’s a pro vs con

Matthew Pettis18:09:14

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 Pettis18:09:13

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).

mikerod18:09:40

Nice that Clara has worked better for you!

mikerod18:09:53

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

mikerod18:09:56

it’s much nicer in cljs

mikerod18:09:27

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

mikerod18:09:54

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

mikerod18:09:05

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

mikerod18:09:18

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

mikerod18:09:33

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

mikerod18:09:50

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

mikerod18:09:03

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

mikerod18:09:47

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 Pettis18:09:25

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.

mikerod18:09:27

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

Matthew Pettis18:09:40

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.

👍 3