clara

Joel 2023-11-09T18:22:38.512869Z

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

ethanc 2023-11-09T18:32:42.330759Z

adding a new binding in an accumulator performs a group-by behavior

2023-11-09T18:32:44.520179Z

How is Kit obtaining more pieces over time? That part confused me in the problem statement

2023-11-09T18:34:19.177029Z

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.

Joel 2023-11-09T18:47:00.236919Z

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

Joel 2023-11-09T18:52:00.223819Z

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.

Joel 2023-11-09T18:53:12.802109Z

@ethanc I’m unclear on your point about “group-by”. Why aren’t both pieces part of the accumulator?

ethanc 2023-11-09T18:56:08.111479Z

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

Joel 2023-11-09T18:57:27.091759Z

But doesn’t it return all the WindSpeeds for a location? Not just 1?

Joel 2023-11-09T18:57:50.166449Z

I’m guessing I need to change acc/all somehow.

Joel 2023-11-09T18:59:52.327349Z

Maybe get rid of the some part from the accumulator and move it to the acc/all logic.

ethanc 2023-11-09T18:59:53.484419Z

> 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

ethanc 2023-11-09T19:00:32.488769Z

kind in this case, is not something that you appear to wish to group by though

ethanc 2023-11-09T19:01:26.469999Z

unless im missing something (i usually do, please correct me)

Joel 2023-11-09T19:03:46.227689Z

I want the same behavior as Windspeed, but it gives a max, I want all (the ones in the kit list).

Joel 2023-11-09T19:04:26.771399Z

I think that some condition is saying, yeah, i got one for you, but i need to filter in the reducer fn.

ethanc 2023-11-09T19:04:50.601939Z

[?assembly <- (acc/all) :from [Piece (some #(= kind %) ?pieces)]]
perhaps then

1
Joel 2023-11-09T19:05:15.363549Z

ill try that.

Joel 2023-11-09T19:06:14.580729Z

Yes! That did it, I suspected the (= ?kind kind) was superfluous but it changed the logic, didn’t realize that.

ethanc 2023-11-09T19:06:37.597289Z

that grouping behavior is a common “gotcha”

ethanc 2023-11-09T19:06:47.416019Z

even for experienced users

➕ 1
ethanc 2023-11-09T19:07:06.628659Z

been bit a couple times myself… burned into my memory

Joel 2023-11-09T19:08:48.383959Z

OK that and adding the “count :test” above makes it work just as I intended.

❤️ 1
2023-11-09T19:10:30.997739Z

If you are using normal insert! (truth maintenance system maintained), your RHS facts would reflect the eventual final state of the working memory

2023-11-09T19:10:34.919589Z

So that’s how you know it is “complete”

2023-11-09T19:11:16.273119Z

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.

Joel 2023-11-09T21:02:16.722849Z

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

2023-11-09T21:17:34.723949Z

In above ?each is a collection of Kit’s

2023-11-09T21:18:59.349539Z

Actually missed the whole point

2023-11-09T21:19:42.167659Z

whhops didn’t do RHS right

2023-11-09T21:20:29.288689Z

You don’t have these Piece’s yet 😆

2023-11-09T21:21:55.348289Z

So you have to be careful to not get into a logical loop

2023-11-09T21:29:08.246259Z

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

2023-11-09T21:31:48.207039Z

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

2023-11-09T21:32:13.397719Z

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.

2023-11-09T21:32:46.257159Z

Instead it’d be nice if Kit had an :id of some sort and we could decompose it into individual KitRequiredPiece <id> <kind> facts.

✔️ 1
2023-11-09T21:33:10.838869Z

Not 100% necessary though. I’d probably at least abstract (some #(= kind %) ?pieces) fn though (although the overhead is still there)

2023-11-09T21:33:37.036629Z

Even just a Kit :id alone would be nicer

2023-11-09T21:34:56.113139Z

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.

Joel 2023-11-09T21:52:38.514519Z

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.

👍 1
2023-11-09T22:31:19.069209Z

https://gist.github.com/mrrodriguez/09df52e5f9729992c61f21fc638a20e6https://gist.github.com/mrrodriguez/6a6f8373b25d69826b3efe154c928fac Not the exact same topic but a related topics conceptually. Maybe helpful to see a similar idea template.

2023-11-09T22:32:54.792429Z

And even further removed but still related: https://www.metasimple.org/2017/12/23/clara-updating-facts.html