oracle-samples/clara-rules

Activation groups in Clojurescript?

Closed this issue · 15 comments

Confused about whether activation-group-fn and activation-sort-fn has been implemented yet in CLJS.

I came across this in https://github.com/cerner/clara-rules/blob/master/src/main/clojure/clara/rules.cljc#L248:

    ;; ClojureScript implementation doesn't support salience yet, so
    ;; no activation group functions are used.

However, a few lines down, vars for activation-group-fn and activation-sort-fn are both passed to (eng/local-memory) inside the LocalSession being assembled.

I'm fairly confident I've witnessed rules marked with {salience: -100} fire last in CLJS. Not positive but pretty sure.

Also saw this comment from Ryan #174 (comment) about the above comment being potentially out of data.

Went ahead and tested activation-group-fn and activation-group-sort-fn in CLJS and both appear to be working. 😃

Yeah, it looks like the comment is out date.

  • The memory uses a sorted map on the activation group sort fn for giving the next rule activation to fire. See to-transient and pop-activation!, among others.
  • ClojureScript sessions wrap the activation-group-fn and activation-group-sort-fn in the same way as [Clojure sessions | https://github.com/cerner/clara-rules/blob/0.14.0/src/main/clojure/clara/rules/compiler.clj#L1714].

Assuming I'm not missing something, we should add some simple test case(s) validating that salience works in ClojureScript if we don't have them already and remove the comment.

Just tried to get a CLJS repl going for a couple hours but don't seem to be getting very far. I see austin and piggieback referenced in test_rules.cljs and removed the comment but they don't seem to be part of the project's dependencies. Should I be trying to add austin and/or piggieback dependencies to my lein profile? Any help would be greatly appreciated. Would love to contribute the tests for CLJS.

I'm able to start a ClojureScript REPL in the clara-rules project by running

lein figwheel

then opening this page in the resources folder in a local browser. I'm not sure what the history of that comment about Piggieback and Austin is; I'd need to look into it more. Figwheel support was added in https://github.com/cerner/clara-rules/pull/179/files

Must be doing something wrong. Just tried on a freshly-cloned repo. Will try to work it out. Thanks.

$ lein figwheel
Figwheel: Validating the configuration found in project.clj

Figwheel: Configuration Valid. Starting Figwheel ...
Figwheel: Starting server at http://localhost:3449
Figwheel: Watching build - figwheel
Compiling "resources/public/js/simple.js" from ["src/test/clojurescript" "src/test/common"]...
WARNING: Use of undeclared Var cljs.core/macroexpand at line 33 resources/public/js/out/clojure/reflect.cljs
WARNING: macroexpand already refers to: cljs.core/macroexpand being replaced by: clojure.reflect/macroexpand at line 33 resources/public/js/out/clojure/reflect.cljs
{:file "/Users/alexdixon/clara-rules/src/test/common/clara/test_common.cljc", :line 28, :column 1, :tag :cljs/analysis-error}
ANALYSIS ERROR: java.lang.RuntimeException: Unable to resolve symbol: test-rule in this context, compiling:(/private/var/folders/2g/sfp74ftj6_q1vw51ytjbgvph0000gn/T/form-init6466183524048984238.clj:1:205) at line 28 /Users/alexdixon/clara-rules/src/test/common/clara/test_common.cljc on file /Users/alexdixon/clara-rules/src/test/common/clara/test_common.cljc, line 28, column 1
clojure.lang.ExceptionInfo: Error in component :figwheel-system in system com.stuartsierra.component.SystemMap calling #'com.stuartsierra.component/start {:reason :com.stuartsierra.component/component-function-threw-exception, :function #'com.stuartsierra.component/start, :system-key :figwheel-system, :component #figwheel_sidecar.system.FigwheelSystem{:system #object[clojure.lang.Atom 0x12fbf42 {:status :ready, :val #<SystemMap>}]}, :system #<SystemMap>}
        at clojure.core$ex_info.invoke(core.clj:4593)
        at com.stuartsierra.component$try_action.invoke(component.cljc:119)
        at com.stuartsierra.component$update_system$fn__14966.invoke(component.cljc:139)
        at clojure.lang.ArraySeq.reduce(ArraySeq.java:109)
        at clojure.core$reduce.invoke(core.clj:6518)
        at com.stuartsierra.component$update_system.doInvoke(component.cljc:135)
        at clojure.lang.RestFn.invoke(RestFn.java:445)
        at com.stuartsierra.component$start_system.invoke(component.cljc:163)
        at com.stuartsierra.component$start_system.invoke(component.cljc:161)
        at com.stuartsierra.component.SystemMap.start(component.cljc:178)
        at figwheel_sidecar.system$start_figwheel_BANG_.invoke(system.clj:588)
        at figwheel_sidecar.repl_api$start_figwheel_from_lein.invoke(repl_api.clj:137)
        at user$eval16683.invoke(form-init6466183524048984238.clj:1)
        at clojure.lang.Compiler.eval(Compiler.java:6782)
        at clojure.lang.Compiler.eval(Compiler.java:6772)
        at clojure.lang.Compiler.load(Compiler.java:7227)
        at clojure.lang.Compiler.loadFile(Compiler.java:7165)
        at clojure.main$load_script.invoke(main.clj:275)
        at clojure.main$init_opt.invoke(main.clj:280)
        at clojure.main$initialize.invoke(main.clj:308)
        at clojure.main$null_opt.invoke(main.clj:343)
        at clojure.main$main.doInvoke(main.clj:421)
        at clojure.lang.RestFn.invoke(RestFn.java:421)
        at clojure.lang.Var.invoke(Var.java:383)
        at clojure.lang.AFn.applyToHelper(AFn.java:156)
        at clojure.lang.Var.applyTo(Var.java:700)
        at clojure.main.main(main.java:37)
Caused by: clojure.lang.ExceptionInfo: Error in component autobuild-figwheel in system com.stuartsierra.component.SystemMap calling #'com.stuartsierra.component/start {:reason :com.stuartsierra.component/component-function-threw-exception, :function #'com.stuartsierra.component/start, :system-key "autobuild-figwheel", :component #figwheel_sidecar.components.cljs_autobuild.CLJSAutobuild{:build-config {:id "figwheel", :source-paths ["src/test/clojurescript" "src/test/common"], :figwheel {:build-id "figwheel"}, :build-options {:main "clara.test", :output-to "resources/public/js/simple.js", :output-dir "resources/public/js/out", :asset-path "js/out", :optimizations :none}, :compiler-env #object[clojure.lang.Atom 0x701dc4a0 {:status :ready, :val #CompilerEnv{}}]}, :figwheel-server #figwheel_sidecar.components.figwheel_server.FigwheelServer{:connection-count #object[clojure.lang.Atom 0x52d77bcd {:status :ready, :val {}}], :browser-callbacks #object[clojure.lang.Atom 0x2e7e0b7a {:status :ready, :val {}}], :ring-handler nil, :file-change-atom #object[clojure.lang.Atom 0x6724cba {:status :ready, :val ()}], :compile-wait-time 10, :builds {"figwheel" {:id "figwheel", :source-paths ["src/test/clojurescript" "src/test/common"], :figwheel {:build-id "figwheel"}, :build-options {:main "clara.test", :output-to "resources/public/js/simple.js", :output-dir "resources/public/js/out", :asset-path "js/out", :optimizations :none}, :compiler-env #object[clojure.lang.Atom 0x701dc4a0 {:status :ready, :val #CompilerEnv{}}]}, "simple" {:id "simple", :source-paths ["src/test/clojurescript" "src/test/common"], :build-options {:output-to "target/js/simple.js", :optimizations :whitespace, :output-dir "target/js/out"}, :compiler-env #object[clojure.lang.Atom 0x18d4b010 {:status :ready, :val #CompilerEnv{}}]}, "advanced" {:id "advanced", :source-paths ["src/test/clojurescript" "src/test/common"], :build-options {:output-to "target/js/advanced.js", :optimizations :advanced, :output-dir "target/js/out"}, :compiler-env #object[clojure.lang.Atom 0x7134b9d4 {:status :ready, :val #CompilerEnv{}}]}}, :server-port 3449, :resolved-ring-handler nil, :server-ip nil, :cljs-build-fn #object[figwheel_sidecar.components.cljs_autobuild$eval16433$figwheel_start_and_end_messages__16436$fn__16438 0x3b4cd78c "figwheel_sidecar.components.cljs_autobuild$eval16433$figwheel_start_and_end_messages__16436$fn__16438@3b4cd78c"], :http-server #object[clojure.lang.AFunction$1 0x75b190f "clojure.lang.AFunction$1@75b190f"], :open-file-command nil, :unique-id "/Users/alexdixon/clara-rules", :log-writer #object[java.io.BufferedWriter 0x5af65edf "java.io.BufferedWriter@5af65edf"], :file-md5-atom #object[clojure.lang.Atom 0x715e4ad8 {:status :ready, :val {}}], :http-server-root "public"}}, :system #<SystemMap>}
        at clojure.core$ex_info.invoke(core.clj:4593)
        at com.stuartsierra.component$try_action.invoke(component.cljc:119)
        at com.stuartsierra.component$update_system$fn__14966.invoke(component.cljc:139)
        at clojure.lang.ArraySeq.reduce(ArraySeq.java:114)
        at clojure.core$reduce.invoke(core.clj:6518)
        at com.stuartsierra.component$update_system.doInvoke(component.cljc:135)
        at clojure.lang.RestFn.invoke(RestFn.java:445)
        at com.stuartsierra.component$start_system.invoke(component.cljc:163)
        at com.stuartsierra.component$start_system.invoke(component.cljc:161)
        at com.stuartsierra.component.SystemMap.start(component.cljc:178)
        at com.stuartsierra.component$eval14907$fn__14908$G__14897__14910.invoke(component.cljc:5)
        at com.stuartsierra.component$eval14907$fn__14908$G__14896__14913.invoke(component.cljc:5)
        at clojure.lang.Atom.swap(Atom.java:37)
        at clojure.core$swap_BANG_.invoke(core.clj:2238)
        at figwheel_sidecar.system.FigwheelSystem.start(system.clj:119)
        at com.stuartsierra.component$eval14907$fn__14908$G__14897__14910.invoke(component.cljc:5)
        at com.stuartsierra.component$eval14907$fn__14908$G__14896__14913.invoke(component.cljc:5)
        at clojure.lang.Var.invoke(Var.java:379)
        at clojure.lang.AFn.applyToHelper(AFn.java:154)
        at clojure.lang.Var.applyTo(Var.java:700)
        at clojure.core$apply.invoke(core.clj:632)
        at com.stuartsierra.component$try_action.invoke(component.cljc:117)
        ... 25 more
Caused by: java.io.FileNotFoundException: resources/public/js/simple.js (No such file or directory)
        at java.io.FileInputStream.open0(Native Method)
        at java.io.FileInputStream.open(FileInputStream.java:195)
        at java.io.FileInputStream.<init>(FileInputStream.java:138)
        at clojure.java.io$fn__9189.invoke(io.clj:229)
        at clojure.java.io$fn__9102$G__9095__9109.invoke(io.clj:69)
        at clojure.java.io$fn__9201.invoke(io.clj:258)
        at clojure.java.io$fn__9102$G__9095__9109.invoke(io.clj:69)
        at clojure.java.io$fn__9163.invoke(io.clj:165)
        at clojure.java.io$fn__9115$G__9091__9122.invoke(io.clj:69)
        at clojure.java.io$reader.doInvoke(io.clj:102)
        at clojure.lang.RestFn.invoke(RestFn.java:410)
        at clojure.lang.AFn.applyToHelper(AFn.java:154)
        at clojure.lang.RestFn.applyTo(RestFn.java:132)
        at clojure.core$apply.invoke(core.clj:632)
        at clojure.core$slurp.doInvoke(core.clj:6653)
        at clojure.lang.RestFn.invoke(RestFn.java:410)
        at figwheel_sidecar.build_middleware.injection$require_connection_script_js.invoke(injection.clj:128)
        at figwheel_sidecar.build_middleware.injection$append_connection_init_BANG_.invoke(injection.clj:135)
        at figwheel_sidecar.build_middleware.injection$hook$fn__15969.invoke(injection.clj:142)
        at figwheel_sidecar.components.cljs_autobuild$eval16433$figwheel_start_and_end_messages__16436$fn__16438.invoke(cljs_autobuild.clj:47)
        at figwheel_sidecar.components.cljs_autobuild.CLJSAutobuild.start(cljs_autobuild.clj:140)
        at com.stuartsierra.component$eval14907$fn__14908$G__14897__14910.invoke(component.cljc:5)
        at com.stuartsierra.component$eval14907$fn__14908$G__14896__14913.invoke(component.cljc:5)
        at clojure.lang.Var.invoke(Var.java:379)
        at clojure.lang.AFn.applyToHelper(AFn.java:154)
        at clojure.lang.Var.applyTo(Var.java:700)
        at clojure.core$apply.invoke(core.clj:632)
        at com.stuartsierra.component$try_action.invoke(component.cljc:117)
        ... 45 more
Subprocess failed

Not sure whether to open another issue for CLJS repl/figwheel as the problems seem isolated to me.

I was able to get a CLJS REPL by using a config similar to what yogthos uses in Luminus. I've copied it below. Let me know if you'd like a PR for it.

tl;dr It tells figwheel to start an nREPL server on a specific port that you can connect to remotely. I use IntelliJ and this works well.

Index: project.clj
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- project.clj	(date 1493692261000)
+++ project.clj	(revision )
@@ -6,8 +6,12 @@
   :dependencies [[org.clojure/clojure "1.7.0"]
                  [prismatic/schema "1.0.1"]]
   :profiles {:dev {:dependencies [[org.clojure/math.combinatorics "0.1.3"]
-                                  [org.clojure/data.fressian "0.2.1"]]}
-             :provided {:dependencies [[org.clojure/clojurescript "1.7.170"]]}}
+                                  [org.clojure/data.fressian "0.2.1"]
+                                  [com.cemerick/piggieback "0.2.2-SNAPSHOT"]
+                                  [figwheel-sidecar "0.5.2"]]}
+             :provided {:dependencies [[org.clojure/clojurescript "1.7.170"]]}
+             :source-paths ["dev"]
+             :repl-options {:init-ns user}}
   :plugins [[lein-codox "0.9.0" :exclusions [org.clojure/clojure]]
             [lein-javadoc "0.2.0" :exclusions [org.clojure/clojure]]
             [lein-cljsbuild "1.1.3" :exclusions [org.clojure/clojure]]
@@ -25,6 +29,9 @@
   :javac-options ["-target" "1.6" "-source" "1.6"]
   :clean-targets ^{:protect false} ["resources/public/js" "target"]
   :hooks [leiningen.cljsbuild]
+  :figwheel
+  {:nrepl-port 7002
+   :nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}
   :cljsbuild {:builds [;; Simple mode compilation for tests.
                        {:id "figwheel"
                         :source-paths ["src/test/clojurescript" "src/test/common"]
Index: dev/user.clj
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- dev/user.clj	(revision )
+++ dev/user.clj	(revision )
@@ -0,0 +1,15 @@
+(ns user
+    (:require [clojure.tools.namespace.repl :refer [refresh]]
+              [figwheel-sidecar.repl-api :as ra]))
+
+(defn start-fw []
+  (ra/start-figwheel!))
+
+(defn stop-fw []
+  (ra/stop-figwheel!))
+
+(defn cljs []
+  (ra/cljs-repl))
+
+(defn reload []
+    (refresh))

Not sure whether to open another issue for CLJS repl/figwheel as the problems seem isolated to me.

It seems like an isolated issue to me. I messed around with setting up the project for figwheel in the past without making a first-class issue out of it and that was a mistake because I didn't as well as it should and it just was too much extra stuff on a separate issue.

However, I think it is fairly straightforward to get figwheel to setup - also with nREPL support, which is often nice.

I think your paste above covers most of it, but with perhaps a few more changes than necessary.
https://github.com/bhauman/lein-figwheel/wiki/Using-the-Figwheel-REPL-within-NRepl does a fairly thorough job of explaining the setup.

Just to enumerate the parts I think are important, I'll write it here.

It does come down to having:

  • :plugins lein-cljsbuild and lein-figwheel
  • [:profiles :dev] dependency on figwheel-sidecar & com.cemerick/piggieback
  • [:profiles :dev] option of :repl-options {:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}
  • Then whatever :figwheel config is needed in the cljsbuild section of the project, which there are templates for.

Typically you put a dev/user.clj file on the [:profiles :dev] path via :source-paths ["src" "dev"]. The user ns will be loaded when the repl starts. That's where you define the file you mentioned above.

@alex-dixon : I've seen the error

"ANALYSIS ERROR: java.lang.RuntimeException: Unable to resolve symbol: test-rule in this context, compiling:(/private/var/folders/2g/sfp74ftj6_q1vw51ytjbgvph0000gn/T/form-init6466183524048984238.clj:1:205) at line 28 /Users/alexdixon/clara-rules/src/test/common/clara/test_common.cljc on file /Users/alexdixon/clara-rules/src/test/common/clara/test_common.cljc, line 28, column 1" intermittently in my ClojureScript build as well. For me, it is consistently resolved by deleting the /target folder in the Clara project and rebuilding. I don't know whether this is due to a problem in Clara's CLJS build setup or a bug somewhere in the build tools stack Clara is using. The error is invariably to resolve test-rule at that location, rather than some other symbol or location.

I created a new issue to track on the Figwheel enhancements discussed above.

Is there anything else you'd like to address on this issue before closing it @alex-dixon ?

@WilliamParker Sorry for being out of the loop, work unfortunately required me to step away from my Clojure-related activities.

Everything looks good. Thank you for opening that issue I really appreciate it.

I've noticed in CLJS I can set up the following groups:

[:action :calc :report :cleanup]

and have a rule in calc insert a fact that ends up being responded to by the action group. I have it on my todo list to investigate whether:
a. That's the way it's supposed to be
b. It works the way I expected in CLJ (that once we pass through :action, we're done with that)
c. I can only observe the behavior in CLJS

I just discovered this the other day. I'm not sure you want to hold the PR or not. Let me know what more information I can provide and I'll be sure to get it to you ASAP if you do think it should hold things up.

Edit: This is an issue, not a PR, and I'm guessing the PR was merged. I am out of the loop :( I apologize.

So if I understand the question correctly, you're asking about a case like this?

(defrule rule1
    {:salience 100}
    [FactB]
     =>
     (insert! FactC))

(defrule rule2
  {:salience -100}
   [FactA]
   =>
   (insert! FactB))

If you insert FactA in a session with both rules I'd expect to have both a FactB and a FactC in the session.

Clara will continue activating rules until there are no rules left to activate. A higher-salience rule can insert a fact that activates a lower-salience rule. What salience does do is ensure that if, say, rule1 and rule2 are both eligible for activation at a given point and rule1 has higher salience that rule1 will be fired before rule2. Note that "fired" means execution of the RHS; Clara will perform some matching for lower salience rules and essentially add the RHS in a queue to be executed. The implementation of that queuing is here.

If Clara didn't behave that way, truth maintenance would be broken. The contract there is that, if you use truth maintenance, the user should be able to not be concerned with execution order or other procedural concerns. If you're using truth maintenance and use the ultimate state of the rules session to drive logic e.g. via queries, then using salience is purely a performance optimization. This isn't the case if you perform unconditional insertions or RHS retractions, in which case you're essentially deciding to use a procedural paradigm and thus likely need to control your execution order.

@WilliamParker Perfect, I've misunderstood then, and all appears to be working as intended.

I've read around quite a bit and your explanation is the first time I think I've finally got it. Thank you.

My implementation uses that list of named groups, then compares salience. What I understand now is that doesn't alter the underlying behavior (duh). Try as I might I'm still specifying precedence, not order.

Seems like I'm really looking for agenda groups / focus (#107), or a way to achieve the same thing. My understanding of that is: Group 1 rules fire, then when done, Group 2 rules fire, and it can never be Group 1's turn again until another call to fire rules.

I'm still mulling over your strong insert logical stance. My guess is my use case is different from yours/Cerner's? I'm inserting new facts from outside the session pretty much constantly. I could see it working if I inserted everything in one go.

In Cerner's use cases rules are almost always statements of logical relationships that make sense on their own, outside of any concerns for execution order. I realize that is something of a vague statement, so I'll give a concrete simplified example (I can't actually lift examples out of our codebase for obvious reasons):

(defn is-systolic-bp? ....)
(defn is-diastolic-bp? ....)

(defrule systolic-bp
        [?r <- Result (is-systolic-bp? this)]
       =>
      (insert! (->SystolicBp ....)))

(defrule diastolic-bp
      [?r <- Result (is-diastolic-bp? this)]
      =>
       (insert! (->DiastolicBp ...))

(defrule join-bps-together
    [?sys <- :from (custom-sys-bp-accumulator) SystolicBp]
    [?dia <- :from (custom-dia-bp-accumulator) DiastolicBp]
   =>
   (insert! (->JoinedBp)))

(defrule needs-bp-treatment
   [?bp <- JoinedBp (> x sys-value) (> y dia-value)]
   [:not [EvaluatedForBpMedication]]
   =>
  (insert! (->DidNotTreatHighBpAppropriately))

This is a highly simplified/redacted example, but the point is that each of these things are logical statements in and of themselves. If a patient has a certain type of entry in their medical record they have a diastolic or systolic blood pressure reading at a given time, provider, etc. If they have a diastolic and systolic reading with certain similarities, they have a full blood pressure reading consisting of those two. If they have an unhealthy blood pressure and they weren't evaluated for treatment, then they weren't treated appropriately. If we need to add additional logic using these conclusions, we can do so. For example, we could add data sources that would be a combined blood pressure without the need for a join, new data sources that would denote a systolic or diastolic blood pressure, etc. This sort of mix-and-match of derived data is fairly common for us. The end result is that queries on our rules sessions return derived knowledge from our input data. Overall I find that this makes our codebase considerably easier to understand than if we took a more procedural approach.

In this context, for semantics to depend on salience doesn't really make sense. We do use it, but in most cases it is purely a performance optimization. I'm not familiar with your use case obviously, but I can see where that might not be the desired behavior if using Clara as more of an event processing engine than this sort of knowledge derivation though. We could consider adding the sort of functionality you describe, but that seems like a significant decision and one that would need some careful thought on (and probably another issue or perhaps mailing list discussion).

The behavior you describe could probably be created in user code with some use of blocker facts inserted between levels, e.g. a fact indicating that the first level was done firing. The rules in the first level would have a negation condition on them that that blocker fact was not present. Those blocker facts would then need to be cleaned up after everything was done firing. I don't have a good understanding of why inserting all at once versus in a stream would make a difference though. Since a constant stream of facts isn't a pattern Cerner has much of, I'd not be at all surprised if there are performance issues to be addressed there, but those wouldn't necessarily need to impact Clara's semantics. Efficiently returning differences between queries relative to the last rules firing rather than all results at a given time would probably be feasible to implement for example.

It looks to me like everything in this issue has either been resolved or will be tracked on by #297

@alex-dixon is there anything else you want to address on salience/activation groups before closing this issue?

@WilliamParker No sir. Thank you.