Fork me on GitHub
#clara
<
2017-11-13
>
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 ?

mikerod17:11:40

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

mikerod17:11:05

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

mikerod17:11:25

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

mikerod17:11:04

In this case, it isn’t too useful

mikerod17:11:21

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 !

mikerod17:11:28

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

mikerod17:11:10

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 🙂

mikerod18:11:44

@dave.dixon that is surprising to me

mikerod18:11:45

I want to take a look

mikerod19:11:28

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

mikerod19:11:40

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

mikerod19:11:43

worksaround it

mikerod19:11:56

I’m not sure the cause still

sparkofreason19:11:29

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

mikerod19:11:30

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

mikerod19:11:56

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

zylox19:11:18

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

mikerod19:11:35

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

mikerod19:11:58

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

mikerod19:11:42

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

mikerod19:11:00

And it’s children are the accumulate nodes

mikerod19:11:34

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

mikerod19:11:49

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

mikerod19:11:46

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

mikerod19:11:34

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))
(comment
  ({: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))
(comment
  ({: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}))


mikerod19:11:42

Unless I’m missing something else prior to this

zylox19:11:30

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

zylox20:11:36

and when they are bound, this fn https://github.com/cerner/clara-rules/blob/master/src/main/clojure/clara/rules/compiler.clj#L790 will consider it satisfied because #{} is a subset of #{?ships ?destroyed}, meaning newly-satisfied will contain the test constraint (https://github.com/cerner/clara-rules/blob/master/src/main/clojure/clara/rules/compiler.clj#L797) and be put in the front of the order in the into at the end https://github.com/cerner/clara-rules/blob/master/src/main/clojure/clara/rules/compiler.clj#L828

zylox20:11:10

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

sparkofreason20:11:31

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

wparker20:11:33

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: https://gist.github.com/WilliamParker/a3adaa3db730229bc01586009bb7178a

wparker20:11:55

Join-filter-equals just dispatches to =:

wparker20:11:10

(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

wparker20:11:26

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

wparker20:11:54

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

wparker20:11:20

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

mikerod22:11:19

@wparker thanks for making it

mikerod22:11:30

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

mikerod22:11:44

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

wparker22:11:46

Feel free to comment on it with thoughts anyone 🙂

wparker22:11:18

I haven’t dug too deeply into it yet