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...
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
@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
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
Different error message though... I keep trying permutations and am having a hard time tracking what each means...
you would still want to qualify the position based constructor function in the RHS of the rule
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?
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)
()
(But I see that that is not related to my namespace issue... I'm doing something wrong with just basic rules, I think...)
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
(r/insert-all [(nsa/->MyRec :init)])
however this will cause an infinite loop do to therec
inserting a fact that then triggers itself
*due
ach, yeah, i copied that over from a place where I was inserting a list, thanks for the catch!
For posterity, here is a working example where I'm importing the defrec
from another namespace... And thank you @ethanc for the great help!
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}})
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.
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)
()
Well, I know it is my logic, not the namespace stuff... this version works, I'll work on the one above...
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}})
Fixed session from above:
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}})
@matthew.pettis it’s just clj rules here for how you refer to record types
in cljs you use ns’s and aliases like your stuff above
in clj (unfortunately), you have to use interop
so the actual :import
class symbols
nothing from a :require
+ :as
etc
@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?
@matthew.pettis I do not like it. I think when writing non-interop clj, things like :import
should not be necessary at all
I don’t like that Clara basically enforces this needing to be done for records
Clj does offer some “factory functions” it auto-creates with defrecord
- which allows you to not have to use interop forms to create records
What clj does not offer, is how to refer to records in a way without interop (eg. :import
)
More frustrating to me, is CLJS does allow you to refer to record types via :require
+ :as
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
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
I think probably, Clara could attempt to support the cljs style record syntax
like a/MyRec
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
this could be convenient for Clara’s perspective. it would deviate from the perspective of what clj actually allows
so there’s a pro vs con
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.
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).
Nice that Clara has worked better for you!
and I really wish clj would have supported non-interop ways to refer to records
it’s much nicer in cljs
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
but yeah, feel free to log a clara-rules github issue if you want to propose allowing the syntactic forms perhaps
the only thing I don’t like as much as it differs from how clj resolves symbols
but we do that in other ways throughout forms as well - so maybe not that big of a deal
and records can be quite convenient over maps (although both are supported)
records have another benefit in that clara compiler will “understand field references”
so you can do like [MyRec (= ?x x)]
where x
is a field on MyRec
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)
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.
and lastly, records can have some efficiency gains where some cases that may matter
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.