Fork me on GitHub
#clara
<
2021-06-09
>
Mario C.15:06:52

If you are not going to use bindings in the LHS and only use them in the RHS; Is it okay to just bind the whole fact and then pluck what you need from that fact binding on the RHS. Or is the better approach to just bind everything that you need in LHS to be used in the RHS? Is it a matter of style only or are there trade offs Im not aware of? I lean on the former since it makes the rule's LHS more "digest-able" instead of seeing 10 lines of bindings that dont really do much in terms constraints/conditions

mikerod15:06:23

@mario.cordova.862 As long as you are only using the binding on the RHS and you are not using an accumulator - I think there isn’t a lot of functional difference in terms of the engine operation.

mikerod15:06:41

If you are using an accumulator, you have to be aware of the “unusued” bindings actually forms “partitions”

mikerod15:06:02

[?all <- (acc/all) :from [X (= ?x x)]]

mikerod15:06:22

that’d actually “partition” the accumulations by their x values

mikerod15:06:42

So it looks “unused” perhaps, but really it semantically matters.

mikerod15:06:46

If you ignore that - perhaps not related to your particular usage you have in mind: I’d still favor being more explicit in the LHS because the LHS is in general more structured. It has more ability to be parsed/analyzed by tooling. Everything you do in the RHS is basically just free-form clj forms.

mikerod15:06:09

So my default preference is to express as much as possible to as much granularity as possible in the LHS and minimize the amount the RHS does

Mario C.16:06:58

Is that assuming that ?x was already binded with some other fact? Or if that was all there was to the LHS?

ethanc16:06:32

Might also be worth noting that using bound fields could also be more performant in certain scenarios, ex.

(defrule some-rule 
  [?x <- X]
  [?y <- Y (= y-field (:x-field ?x))]
  =>
  (<RHS things>))
vs
(defrule some-rule 
  [X (= ?x-field x-field)]
  [?y <- Y (= y-field ?x-field)]
  =>
  (<RHS things>))
Probably not a super huge deal for small rules networks, but the latter would likely produce a HashJoin node rather than an ExpressionJoinNode. ExpressionJoinNodes, in my experience, have a bit more overhead (minuscule) due to additional function evaluations and invocations.

Mario C.16:06:57

(defrule my-rule
  [:x/y :from [{:keys [a b c d e f g h i j]}] (= a ?a) . . . (= j ?j)]
  =>
  (insert! {:data {:my-a ?a ... :my-j ?j}}))

Mario C.16:06:02

This is what I had in mind

Mario C.16:06:42

vs

(def my-rule 
  [?xy <- :from [:x/y] ...)

Mario C.16:06:38

(ignore syntax errors)

ethanc16:06:22

As Mike mentioned the top rule will perform a partitioning action, with potentially different behavior. If the number of fields being bound is equivalent to the number of fields of the fact, then simply binding the entire fact probably makes just as much sense.

Mario C.16:06:41

hmm very interesting, I did not know that. Thanks for the input @ethanc @mikerod. Will need to do some exploring then

Mario C.16:06:58

(defrule binded-rule
  [:foo/bar [{:keys [id first-name last-name]}]
   (= id ?id)
   (= first-name ?first-name)
   (= last-name ?last-name)]
  =>
  (println "fire bind")
  (insert! {:fact/type  :binded/person
           :id         ?id
           :first-name ?first-name
            :last-name  ?last-name}))

(defrule unbinded-rule
  [?person <- :foo/bar]
  =>
  (println "Fired unbind")
  (insert! {:fact/type  :unbinded/person
            :id         (:id ?person)
            :first-name (:first-name ?person)
            :last-name  (:last-name ?person)}))

(defrule binded-acc-rule
  [?persons <- (acc/all) :from [:foo/bar [{:keys [id]}] (= id ?id)]]
  =>
  (println "BINDED ACC:")
  (println ?persons)
  (insert! {:fact/type  :binded/acc
            :some :foo}))

(defrule unbinded-acc-rule
  [?persons <- (acc/all) :from [:foo/bar]]
  =>
  (println "UNBINDED ACC:")
  (println ?persons)
  (insert! {:fact/type  :unbinded/acc
            :some :bar}))
The first two didn't really see any difference aside from the binded rule having constraints. But the last two def had difference. The binded-acc-rule fired 3 times for each 3 facts and the unbinded just fired once, accumulating all 3 facts. I understand now what you meant by partition

mikerod17:06:58

@mario.cordova.862 yes the accumulator cases is waht I was mentioning

mikerod17:06:06

Accumulators (necessarily) have to be “partitioned” by the binding

mikerod17:06:33

To understand why, consider this

[?persons <- (acc/all) :from [:foo/bar [{:keys [id]}] (= id ?id)]]
=>
(println [?id ?persons])

mikerod17:06:55

Notice how if you were referring to the ?id binding itself - it would need to have a single-stable value for that accumulation of facts?

mikerod17:06:19

So (acc/all) must only work across “equal join binding partitions”

mikerod17:06:25

The same goes for any accumulator

mikerod17:06:29

Also, as Ethan said https://clojurians.slack.com/archives/C08TC9JCS/p1623255042062600 and I just left out since you said “unused LHS bindings” - I’d certainly favor direct field bindings in join cases due to the rules engine taking advantage of that visibility into the lower-level of granularity

mikerod17:06:44

I can understand how

(defrule my-rule
  [:x/y :from [{:keys [a b c d e f g h i j]}] (= a ?a) . . . (= j ?j)]
  =>
  (insert! {:data {:my-a ?a ... :my-j ?j}}))
may be a bit tedious though

mikerod17:06:11

You could consider perhaps wrapping some sort of helper over it

mikerod17:06:39

Or just bind to the whole fact, but know there are these certain caveats and also the thing I said about expressing as much as possible in LHS

Mario C.17:06:17

Yea when I mentioned the problem I wasn't referring to accumulators but learning this bit actually made me "read" the rules differently now. Always had trouble wrapping my head around having a binding in an accumulator. "If its accumulated which id is the ?id tied to??" but now I see that it fires once for each

👍 2