How do I write the rule below? Kit has a list of pieces, I want this to trigger when all the pieces of the Kit are available.
(defrecord Kit [pieces])
(defrecord Piece [kind])
(r/defrule complete-kit-rule
[Kit (= ?pieces pieces)]
[?assembly <- (acc/all) :from [Piece (= ?kind kind) (some #(= ?kind %) ?pieces)]]
=>
(prn "complete kit"))
(-> (r/mk-session)
(r/insert (->Kit [:a :c]))
(r/insert (->Piece :a))
(r/insert (->Piece :c))
(r/fire-rules))
This fires for each piece (when the kit isn’t actually complete). Adding the following test makes the rule not fire at all:
[:test (= (count ?pieces) (count ?assembly))]
adding a new binding in an accumulator performs a group-by behavior
How is Kit obtaining more pieces over time? That part confused me in the problem statement
However it seems like one approach would be for the RHS to insert an intermediate fact that combined a Kit with its Pieces as done in your rule here. But it also would have a new field/flag indicating if it was “complete”. You’d have then another rule that operated on complete flagged facts to do whatever it is.
@mikerod The Pieces are getting inserted from other rules. I want the Kit to be complete when it has all it’s pieces. The kit length won’t change, but could be various size.
What I find odd is that the rule does not include both pieces in the assembly I get this
"complete kit" [#Piece{:kind :a}]
"complete kit" [#Piece{:kind :c}]
So @mikerod not sure how I could signal a complete kit.@ethanc I’m unclear on your point about “group-by”. Why aren’t both pieces part of the accumulator?
?kind is a new binding in the accumulator, the reason its logged twice in the comment above is because “kind” differs on the two pieces. https://www.clara-rules.org/docs/accumulators/ > Note that we accumulate once per distinct set of bindings. So, for example, if we have temperatures from 3 different locations, the rule get-current-temperature above will fire three times, once per distinct location.
But doesn’t it return all the WindSpeeds for a location? Not just 1?
I’m guessing I need to change acc/all somehow.
Maybe get rid of the some part from the accumulator and move it to the acc/all logic.
> But doesn’t it return all the WindSpeeds for a location? Not just 1? yes, it will return all wind speeds for a location and execute the RHS for each location. Similarly, your rule will collect all pieces by kind and execute the RHS per kind
kind in this case, is not something that you appear to wish to group by though
unless im missing something (i usually do, please correct me)
I want the same behavior as Windspeed, but it gives a max, I want all (the ones in the kit list).
I think that some condition is saying, yeah, i got one for you, but i need to filter in the reducer fn.
[?assembly <- (acc/all) :from [Piece (some #(= kind %) ?pieces)]]
perhaps thenill try that.
Yes! That did it, I suspected the (= ?kind kind) was superfluous but it changed the logic, didn’t realize that.
that grouping behavior is a common “gotcha”
even for experienced users
been bit a couple times myself… burned into my memory
OK that and adding the “count :test” above makes it work just as I intended.
If you are using normal insert! (truth maintenance system maintained), your RHS facts would reflect the eventual final state of the working memory
So that’s how you know it is “complete”
And then a flag could be added to the result type to distinguish it from incompletes in other rules. However, for your specific case, now that I see what you’re doing more, I think the non-grouping acc/all + a :test seems to capture what you want.
In a similar vein, is it possible to iterate over the list of pieces? Say, instead I want to create each piece (insert) if it doesn’t exist that is in the Kit. Something like:
[?each <- (acc/all) :from [Kit (= pieces ?pieces)]]
[:not (Piece (= kind ?each))]
=>
(insert (->Piece ?each))In above ?each is a collection of Kit’s
Actually missed the whole point
whhops didn’t do RHS right
You don’t have these Piece’s yet 😆
So you have to be careful to not get into a logical loop
I think the obvious thing, falls prey to this:
(r/defrule pair-kit-pieces
[Kit (= pieces ?kit-pieces)]
[?pieces <- (acc/all) :from [Piece (some #(= kind %) ?kit-pieces)]]
=>
(let [matched-pieces (set ?pieces)]
(insert (map->KitPieces {:pieces ?pieces
:missing-pieces (filterv matched-pieces ?kit-pieces)}))))
(r/defrule add-missing-kit-pieces
[KitPieces (= ?missing-pieces missing-pieces)]
=>
(r/insert-all! (for [missing ?missing-pieces]
(->Piece missing))))One way out of that, is to use a different fact for a “generated piece” (aka automatically added) vs a regular piece:
(r/defrule pair-kit-pieces
[Kit (= pieces ?kit-pieces)]
[?pieces <- (acc/all) :from [Piece (some #(= kind %) ?kit-pieces)]]
=>
(let [matched-pieces (set ?pieces)]
(insert (map->KitPieces {:pieces ?pieces
:missing-pieces (filterv matched-pieces ?kit-pieces)}))))
(r/defrule add-missing-kit-pieces
[KitPieces (= ?missing-pieces missing-pieces)]
=>
(r/insert-all! (for [missing ?missing-pieces]
(->GeneratedPiece missing))))
(r/defrule assemble-kit
[KitPieces (= ?pieces pieces)]
[?matched-pieces <- (acc/all) :from [Piece (some #(= kind %) ?pieces)]]
[?gen-pieces <- (acc/all) :from [GeneratedPiece (some #(= kind %) ?pieces)]]
=>
(r/insert! (->Assembled (into (vec ?matched-pieces)
?gen-pieces))))I think more could be done here that I’d prefer to clean up. Such as not having a Kit fact have all the pieces collected forcing us to keep using accumulators.
Instead it’d be nice if Kit had an :id of some sort and we could decompose it into individual KitRequiredPiece <id> <kind> facts.
Not 100% necessary though. I’d probably at least abstract (some #(= kind %) ?pieces) fn though (although the overhead is still there)
Even just a Kit :id alone would be nicer
eg.
(r/defrule pair-kit-pieces
[Kit (= pieces ?kit-pieces) (= ?id id)]
[?pieces <- (acc/all) :from [Piece (some #(= kind %) ?kit-pieces)]]
=>
(let [matched-pieces (set ?pieces)]
(insert (map->KitPieces {:id ?id
:pieces ?pieces
:missing-pieces (filterv matched-pieces ?kit-pieces)}))))
(r/defrule add-missing-kit-pieces
[KitPieces (= ?missing-pieces missing-pieces)]
=>
(r/insert-all! (for [missing ?missing-pieces]
(map->GeneratedPiece {:kit-id ?id
:kind missing}))))
(r/defrule assemble-kit
[KitPieces (= ?pieces pieces) (= ?kit-id id)]
[?matched-pieces <- (acc/all) :from [Piece (some #(= kind %) ?pieces)]]
[?gen-pieces <- (acc/all) :from [GeneratedPiece (= ?kit-id kit-id)]]
=>
(r/insert! (map->Assembled {:kit-id ?kit-id
:pieces (into (vec ?matched-pieces)
?gen-pieces)})))
Basically, having something like an “id” helps be more “database”-like, which I think rules typically are.That’s the “conflict” I was struggling a bit with. If I want a rule to fire more than once it’s like I need a separate fact for it versus trying to use accumulators somehow. I could also insert these separate before fire-rules as well in my case. But I think in any case, you’ve definitely delineated the solution space. Thanks.
• https://gist.github.com/mrrodriguez/09df52e5f9729992c61f21fc638a20e6 • https://gist.github.com/mrrodriguez/6a6f8373b25d69826b3efe154c928fac Not the exact same topic but a related topics conceptually. Maybe helpful to see a similar idea template.
And even further removed but still related: https://www.metasimple.org/2017/12/23/clara-updating-facts.html