Fork me on GitHub
#clara
<
2016-01-21
>
pguillebert19:01:24

I’m new to clara-rules and trying to model a rule with a “must not” condition

pguillebert19:01:45

like “do not trigger if there is this object present"

pguillebert19:01:51

is it doable ?

devn21:01:56

@pguillebert: sure, totally doable.

devn21:01:41

@pguillebert:

(defrecord Foo [a])
(defrecord Bar [b])
(defrecord Baz [b])

(defrule foo
  [:not [Foo]]
  [Bar (= ?b b 3)]
  =>
  (insert! (->Baz ?b)))

(defquery q:baz []
  [?baz <- Baz])

(-> (mk-session [foo q:baz])
    (insert-all [;; (->Foo 1)
                  (->Bar 3)])
    (fire-rules)
    (query q:baz))

;; => ({:?baz #crede.rules.examples.Baz{:b 3}})

devn21:01:15

With the same setup, but including a Foo:

(-> (mk-session [foo q:baz])
    (insert-all [(->Foo 1)
                 (->Bar 3)])
    (fire-rules)
    (query q:baz))

;; => ()

devn21:01:37

There is also [:exists [Foo]], but I am not sure I understand the nuance there, though:

pguillebert21:01:52

ok, with :not

devn21:01:02

(defrule foo
  [:exists [Foo]]
  [Bar (= ?b b 3)]
  =>
  (insert! (->Baz ?b)))

(defquery q:baz []
  [?baz <- Baz])

(-> (mk-session [foo q:baz])
    (insert-all [(->Foo 1)
                 (->Bar 3)])
    (fire-rules)
    (query q:baz))

;; => ({:?baz #crede.rules.examples.Baz{:b 3}})

pguillebert21:01:03

I just have to trust the magic

devn21:01:26

@pguillebert: it's definitely taken me some getting used to

devn21:01:15

it's happened more than once now where we wrote some rules, had some initial facts, and then a week or two later realize: "oh, this could be simplified a whole lot"

devn21:01:55

or "hmm, this record type is too general, we should fan out to multiple types of records in this rule"

devn21:01:53

@pguillebert: it really helps to keep an up-to-date graph of the logic, how rules are connected

pguillebert21:01:03

this is really a different way of expressing what I want

devn21:01:41

@ryanbrush: do you have any thoughts on the following:

(defn group
  "Return a generic grouping accumulator. It behaves like
  clojure.core/group-by with the exception of specifying the grouping
  step.

  Parameters:

  value-fn - unary function which returns the grouping key.
  grouping-fn - binary function which recieves the current key
    value the current reduce value and returns a new key value.
  combine-fn - function for clara.rules/accumulator :combine-fn.
  convert-return-fn - function for clara.rules/accumulator :convert-return-fn."
  [{:keys [value-fn grouping-fn combine-fn convert-return-fn]
    :or {value-fn identity
         combine-fn merge
         convert-return-fn identity}}]
  {:pre [(ifn? grouping-fn)]}
  (clara.acc/accum
   {:initial-value {}
    :reduce-fn
    (fn [m x]
      (let [v (value-fn x)]
        (update m v grouping-fn x)))
    :combine-fn combine-fn
    :retract-fn #(throw (RuntimeException. "group retract-fn triggered"))
    :convert-return-fn convert-return-fn}))

(defn group-by
  "Return an accumulator which behaves like clojure.core/group-by."
  [f]
  (group
   {:value-fn f
    :grouping-fn (fnil conj [])
    :combine-fn (fn [a b]
                  (merge-with (comp vec into) a b))}))

devn21:01:52

What should retract-fn be here? /me puzzles

devn21:01:45

perhaps I should be using reduce-to-accum here instead

devn22:01:44

(defn group-by
  [f]
  (clara.acc/reduce-to-accum
   (fn [m x]
     (let [v (f x)]
       (update m v (fnil conj []) x)))
   {}
   identity
   (fn [a b]
     (merge-with (comp vec into) a b))))

ryanbrush22:01:28

@devn retract-fn should "undo" the effect of adding an element to the accumulated result. In this case, I think it's the opposite of a group-by step for a single value...which presumably would be finding that value in its underlying group and removing it. Probably not trivial to implement but seems doable.

devn22:01:51

@ryanbrush: the behavior of group-by with reduce-to-accum seems to be doing what i'm looking for by default. am i crazy?

devn22:01:01

(defrule foo
  [?foo <- (my-group-by :a)
   :from [Foo]]
  [Bar (= ?b b 3)]
  [:test (do (println ?foo)
             ?foo)]
  =>
  (println ?foo)
  (insert! (->Baz ?foo)))

(defquery q:baz []
  [?Qux <- Baz])

(let [f (->Foo 3)]
  (-> (mk-session [foo q:baz])
      (insert-all [(->Foo 1)
                   (->Foo 1)
                   (->Foo 2)
                   f
                   (->Bar 3)])
      (retract f)
      (fire-rules)
      (query q:baz)))

devn22:01:27

prints:

{1 [#myproject.rules.examples.Foo{:a 1} #myproject.rules.examples.Foo{:a 1}], 2 [#myproject.rules.examples.Foo{:a 2}], 3 [#myproject.rules.examples.Foo{:a 3}]}
{1 [#myproject.rules.examples.Foo{:a 1} #myproject.rules.examples.Foo{:a 1}], 2 [#myproject.rules.examples.Foo{:a 2}]}
{1 [#myproject.rules.examples.Foo{:a 1} #myproject.rules.examples.Foo{:a 1}], 2 [#myproject.rules.examples.Foo{:a 2}]}

ryanbrush22:01:02

@devn, no that makes sense. It's probably not the most efficient implementation if you have lots of retracts since it will redo the entire computation. The retract optimization could do something more efficient by removing a single value rather than re-reducing.

devn22:01:21

ahhh, i see what you're getting at

devn22:01:30

the number of retracts here will be really small, so no worries

ryanbrush22:01:18

Yeah, I wouldn't worry about it unless you have lots of retracts and data, and you can always optimize it later without changing visible behavior.

ryanbrush22:01:44

Cool use of accumulator, by the way.

devn22:01:06

I think we talked about this awhile back, but any interest in pulling something like this into clara's accumulators?

devn22:01:30

I suppose you'd be looking for the more efficient impl that undoes a single step

ryanbrush22:01:06

I'd bring that one in if you want to put together a pull req and a simple test.

devn22:01:14

and honestly, I'm not sure if I'll spend the time there... really, this group-by is just a tad more complicated

devn22:01:36

it's nice to group-by over a set of facts, and then call a function on the resulting set to bind to ?x

devn22:01:42

like you might group by date, and then find the minimum date

devn22:01:48

and bind that

ryanbrush22:01:58

Interesting. It's not a flow we've used directly but makes sense to me. I might actually think about doing so in our own rules.

devn22:01:31

@ryanbrush: either way i'll send a PR your way with a simple test and we can talk more there

pguillebert22:01:47

mmmh. apparently this is not working :

pguillebert22:01:51

[:not [:test (and (= ?ndate ?sdate) (= ?ndes ?sdes))]]

pguillebert22:01:21

can I negate tests on variables ?

pguillebert22:01:02

same test applied in a when in the RHS does what I want (prevent rule firing based on :test)

pguillebert22:01:27

I mean, a when-not

pguillebert22:01:53

is this a limitation or am I missing something ?

devn23:01:20

@pguillebert: does it work if you just (not (and ...)) in a [:test ...] without the wrapping [:not ...]?

pguillebert23:01:06

I just killed my emacs simple_smile

pguillebert23:01:25

I didn’t try, will test tomorrow

devn23:01:38

@pguillebert: my guess is you had an infinite loop

devn23:01:58

@pguillebert: feel free to gist code in here if you'd like another set of eyes