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
@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.
If you are using an accumulator, you have to be aware of the “unusued” bindings actually forms “partitions”
[?all <- (acc/all) :from [X (= ?x x)]]
that’d actually “partition” the accumulations by their x
values
So it looks “unused” perhaps, but really it semantically matters.
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.
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
Is that assuming that ?x
was already binded with some other fact? Or if that was all there was to the LHS?
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.(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}}))
This is what I had in mind
vs
(def my-rule
[?xy <- :from [:x/y] ...)
(ignore syntax errors)
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.
(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@mario.cordova.862 yes the accumulator cases is waht I was mentioning
Accumulators (necessarily) have to be “partitioned” by the binding
To understand why, consider this
[?persons <- (acc/all) :from [:foo/bar [{:keys [id]}] (= id ?id)]]
=>
(println [?id ?persons])
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?
So (acc/all)
must only work across “equal join binding partitions”
The same goes for any accumulator
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
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 thoughYou could consider perhaps wrapping some sort of helper over it
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
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