Fork me on GitHub
Geoffrey Gaillard16:11:31

Hi ! I have troubles understanding how destructuring works in clara. If I understand the documentation correctly, I would expect this rule to match any record of type Foo and bind the symbol foo to the record itself, so I can use it afterward.

(defrule do-something-with-foo
  [Foo [foo]]
  (do-something-with  foo))
I always get an Unable to resolve symbol: foo in this context, where context is the RHS. Am I missing something ?


@ggaillard the destructured bound symbols are scoped locally to the expressions of the single condition doing the destructuring bind


You have to bind a variable to use throughout other conditions or the rule and/or the RHS


(defrule do-something-with-foo
  [Foo [foo] (= ?foo foo)]
  (do-something-with  ?foo))


In this case, it isn’t too useful


This would make more sense

(defrule do-something-with-foo
  [?foo <- Foo]
  (do-something-with  ?foo))

Geoffrey Gaillard17:11:00

Oh I get it ! Thank you !


times like:

(defrule do-something-with-foo
  [Foo [{:keys [x y]}] (< 10 x) (= ?y y)]
  (do-something-with  ?y))

are where it may be more useful


Also note that when using Clojure records (i.e. defrecord classes) and/or, on the JVM side, Java POJOs adhering to the Java bean spec, Clara automatically makes the field names visible in the local scope of a condition’s experssions

(defrecord Foo [x y])

(defrule do-something-with-foo
  [Foo (< 10 x) (= ?y y)]
  (do-something-with  ?y))
Would work without the explicit destructuring syntax

Geoffrey Gaillard17:11:28

I was using your last example until now to extract my records' fields, and since I missed the <- part of facts expressions it quickly became inconvenient when I needed to work with every fields … Thanks for this clarification 🙂


@dave.dixon that is surprising to me


I want to take a look


@dave.dixon I think there is some sort of compilation issue happening with that rule


(defrule lost-game
  [?ships <- (acc/count) :from [Ship]]
  [?destroyed <- (acc/count) :from [Destroyed]]
  [:test (do (= ?ships ?destroyed))]
  (insert! (->Loser)))


worksaround it


I’m not sure the cause still


@mikerod Thanks. I had several puzzling cases like this, will try the work-around when I get home


To me it is looking likely to be worthy of logging an issue to clara github though


if you have more examples of different shapes, that’d be interesting too


Is the test node making a binding somehow instead?....thatd be weird


@zylox I think it is misintepreting it somehow along those lines


I looked a a bit in clara.rules.compiler but I don’t see the issue yet


If you look at the rulebase for the network without the do (the problem case), the TestNode is a beta root with no parents


And it’s children are the accumulate nodes


This made me suspicious of the function clara.rules.compiler/sort-conditions which does a topological type of sort of accumulators that rearranges some conditions


The topo sort is supposed ensure that any used variable binding is pushed below it’s creation unification binding condition


Adding the do in the :test node pushes it down as a child of the accumulate nodes in the network and then it works


It may be completely a problem with the sort, as the below demonstrates

(ns test.ns
  (:require [clara.rules :as r]
            [clara.rules.accumulators :as acc]
            [clara.rules.compiler :as com]))

(defrecord Ship [])
(defrecord Destroyed [])
(defrecord Loser [])

(r/defrule lost-game-fixed
  [?ships <- (acc/count) :from [Ship]]
  [?destroyed <- (acc/count) :from [Destroyed]]
  [:test (do (= ?ships ?destroyed))]                           ;; Notice the `do` surrounding form is important for the sort
  (r/insert! (->Loser)))

(r/defrule lost-game-broken
  [?ships <- (acc/count) :from [Ship]]
  [?destroyed <- (acc/count) :from [Destroyed]]
  [:test (= ?ships ?destroyed)]                                  ;; Condition with problem, doesn't work without surrounding form (`do` above)
  (r/insert! (->Loser)))

(com/sort-conditions (-> lost-game-fixed :lhs))
  ({:accumulator (clara.rules.accumulators/count), :from {:type test.ns.Ship, :constraints []}, :result-binding :?ships}
   {:accumulator (clara.rules.accumulators/count), :from {:type test.ns.Destroyed, :constraints []}, :result-binding :?destroyed}
   {:constraints [(do (= ?ships ?destroyed))]}))

(com/sort-conditions (-> lost-game-broken :lhs))
  ({:constraints [(= ?ships ?destroyed)]}
   {:accumulator (clara.rules.accumulators/count), :from {:type test.ns.Ship, :constraints []}, :result-binding :?ships}
   {:accumulator (clara.rules.accumulators/count), :from {:type test.ns.Destroyed, :constraints []}, :result-binding :?destroyed}))


Unless I’m missing something else prior to this


seems to be that classify-variables considers it an equality-expression?, resulting in them being considered "bound", but "unbound" in the workaround case


and when they are bound, this fn will consider it satisfied because #{} is a subset of #{?ships ?destroyed}, meaning newly-satisfied will contain the test constraint ( and be put in the front of the order in the into at the end


im not sure of a good way to fix this. i can see why it thinks its a bound variable.


That fixed it, as well as the other cases, which were essentially the same, with an equality check inside of [:test].


The compilation of this rule does seem to be wrong. When I inspect the rule network the :test condition seems to completely not exist in the network. Gist with the pretty-printed network:


Join-filter-equals just dispatches to =:


(defn join-filter-equals
  "Intended to be a test function that is the same as equals, but is not visible to Clara as such
  and thus forces usage of join filters instead of hash joins"
  [& args]
  (apply = args)) 
it is a helper function for Clara’s tests


Same idea really as wrapping in a do like @mikerod did


Clara has special cases around constraints having = as the first symbol in order to create variable bindings, but when the first symbol is something else if becomes a “normal” call to clojure.core/=. @zylox is probably on the right track here with looking at different paths depending on whether equality-expression? returns true


I’ll need to look more closely at it to have an informed opinion on the exact cause and what to do about it but this does look like a bug to me @dave.dixon You should be able to avoid it for the moment with strategies that prevent = from being the first symbol in a :test constraint for the moment though


@wparker thanks for making it


the only thing maybe worth adding is my comment showing how the sort conditions comes out incorrect


I can add as a comment though, or perhaps it isn’t necessary since everyone looking at it already knows anyways 😛


Feel free to comment on it with thoughts anyone 🙂


I haven’t dug too deeply into it yet