Ah, yes
Looks like we might want to support that...
You'd like to have for insertions correct?
That's how I was using it. Not sure if it's applicable elsewhere.
Have you guys gotten this to work in a "reloadable" fashion? Right now I need to refresh my whole page before the rules are redefined.
Sorry for the bombardment of questions š I really think you guys have hit the nail on the head here for UI development.
@kenny we are loving it, keep it coming š
You have no idea how helpful it is for us š
BTW, I have added a note in issue #28 to make sure we support nested maps (per Kenny's example)
Youāre not alone with having to refresh the page. I believe this is the case with Clara and think it has to do with their CLJS implementation
Iām really not sure though. It seems like the reloading should work even though weāre dealing with macros
Yeah, it certainly seems like it should just work given it's just interning a var.
I'll open an issue so it can be kept in mind. Certainly worth investigating.
Oh thanks I was just about to š
Good issue
Is there anyway to see a list of all facts currently in the session? I see Clara has clara.tools.inspect but that isn't available in CLJS.
Thanks for opening that
Well, one way is to write a print all facts rule.
(rule print-all-facts
[?fact <- [_ :all]]
=>
(println "Fact!" ?fact)
You can see the viewmodel/store state by derefing precept.state/store. They wonāt be in tuple format and but they should be what you have in the session at the end of every fire-rules
Itās also what your components ultimately get
Ah yeah, that is useful.
Which one? š
Latter
I just wanted to inspect the results of my rules after they had been ran.
It'd be cool to have a tool similar to re-frisk to visualize the current and past state.
Is refrisk more tracing though? We were thinking something more similar to redux devtools
It is but I was thinking smaller steps š Making your own redux devtools is quite ambitious š
Hm. Ok. I hear you
We could easily print out āRetracted! xā āAdded! yā
https://github.com/CoNarrative/precept/blob/master/src/clj/precept/listeners.cljc
As a first step, I think that'd be handy.
Awesome
Also, not sure if you guys have a personal or company dev blog but posts about Precept would be great, especially if they were interactive with Klipse.
Of course that depends on how much time you guys have.. š
Sure. Tutorial type posts with examples is what you'd like to see?
alex-dixon: App structure, talking to servers, debugging, testing
mikegai: Interesting, how do you model the FSM concretely, is it a regular entity with facts?
@alex-dixon You mentioned yesterday that you were thinking about rules without names, but it occurred to me this morning that might be difficult to resolve with dynamic loaders like Figwheel.
@weavejester Correct. Weāre already facing an issue similar to that it seems. Page must be refreshed for rule changes to take effect https://github.com/CoNarrative/precept/issues/80
You might notice define
does not accept a name so we have already crossed this precipice for better or worse
@alex-dixon Couldnāt you give defines names as well?
Yes though Iām fairly confident the issue with having to refresh the page predated define
having an autogenerated name
I guess if a rule is redefined, the associated session still has the old rules.
Yes. Was just going to mention if you change the name of a rule you now have two rules
Any idea why a rule would return true after a fact has been retracted?
Given what I just told weavejester I guess make sure you didnāt rename a rule š
Is there another fact that could be satisfying the condition?
The fact simply isn't there. I am checking by printing the store
.
Ok. Would you be able to copy in the rule in question?
Sure. Lemme make an example. Still messing around with the example login form I sent yesterday.
Cool
So the logging-in
rule is being called every time the email or password inputs change.
Ok. Taking a look
Just added the reagent component if that helps as well..
Perfect. Was going to ask š
Ignore the duplicate rule.. š
Hmā¦.well. You might want :logging-in?
to be :transient
So I never explicitly set :logging-in?
to false
but I do retract it.
Not sure that will solve the problem
Well, the problem is do-login
is async.
It technically should be :transient
but all of the rules would run before that request completes.
I assumed it's not recommended to have a long-running synchronous function call in the RHS.
I wouldnāt necessarily say that. I think it depends. Sometimes thatās what you want. Maybe thatās the case here?
Other things could be happening in the UI while that request is going through. Wouldn't that block the rules from continually running?
This is true š
When do you see the retraction happening
When it should -- after inserting :login-response
.
But then everytime :email
or :password
changes from that point on, the logging-in
rule is ran.
It's almost like it's stuck in a cache or something.
And Iām guessing :logging-in
isnāt marked as :one-to-many
in a schema
No.
No schema
(rules/session
app-session
'precept-play.rules)
Ok
Well, Iām having a hard time seeing how ālogging-inā is sticking around. Iād suggest adding a rule that says āif Iām logged in Iām not logging inā
I realize you have this with the response but Iād try adding an additional rule for :logged-in?
Sorry, new rule or change to existing rule?
Iād add a new rule for it
Added this:
(rules/rule not-logging-in-when-logged-in
[[_ :logged-in? true]]
[[_ :logging-in? true]]
=>
(precept.util/retract! [:global :logging-in? true]))
And still getting the same problem.Oh
Ok. Lightbulb
Try
[?fact <- [_ :logging-in? true]]
No slack clj highlighting š¢
For the login response rule add that line
Yeah heh always forget
But yeah try adding that line and retracting ?fact instead of the vector version of it
Worky!
Yes!
Ok so why does my version not work?
Well, it should first off. Sorry for your troubles š
āUnder the coversā all facts are Tuple records that have e a v fact-id, where fact-id is incremented for each fact added to the session
So when retracting I think we do the same thing as inserting. If you supply a vector, we add that incremented fact id for you. So that gets passed to Claraās retract, which happily retracts a fact that doesnāt exist
The fact that does exist is the one in the session with that particular fact id, so you have to match on the whole thing in order to retract it
Makes sense.
š
So using <-
binds the whole tuple from the right hand side to the left?
Yes. This is one of the ways our syntax is like Claraās
Got it. Some more feedback for you guys.. I think an API should limit the number of ways you can do one thing as best as possible. So, I think retract!
, insert!
, etc. should all take a vector of facts rather than a single fact or a vector of facts.
Ok. So for inserting a single fact youād prefer (insert! [[id :bar "baz"]])
?
Most definitely. It's slightly more verbose but far less obtuse.
As an example, Datomic's transact
only takes a list of tx-data, never a single datom/map.
Good example š
Iāve made a note. I made the decision based off my own laziness in response to Clara having insert!
, insert-all!
, insert-unconditional!
, insert-all-unconditional!
, insert
and insert-all
IMO it's much better to just have one function that takes exactly one type of argument.
Yeah, that list of fns is nasty. My intuition tells me that the author did that for some sort of performance reasons. Not sure if that is true or not.. š
I think youāre right. Thanks for the feedback that makes a lot of sense to me
Also I just came across your spectomic library. Thanks for making that, speaking of something thatās been bugging me š
I'm surprised you found that actually
I think me and everyone else have been looking for it for some time now š
Haha maybe. It wasn't clear to me if that was the right solution. I wanted to give it some time internally to see if it actually shakes out well.
Iāve heard it mentioned more than once that people would like the two to converge or be compatible in some way
Oh ok. I was going to ask if youād mind if I tweeted about it š
My main concern was the inherent coupling you make between app level specs and Database schema. Maybe that's the way things typically shake out, maybe not ĀÆ\(ć)/ĀÆ
But yeah you can š
What is your Twitter name?
@a_dixon
Iām very creative š
Anyway, why are rules named? Is it only for documentation purposes?
Was talking to weavejester about this earlier. Weād prefer them not to be but managing that is a little tricky and I ran out of time
Ah, yes I see that. Yeah, that definitely seems like something that should be figured out. Consistency is important š
Yeah. define
seems to be working. There are some larger issues that weād like to solve there like not multiplying rules when you rename them and not having to refresh the page for a change to a rule to take effect
I think you opened the issue for that yesterday
Right, right.
Though, I think a name and/or a docstring would be important to have as well
Would you be ok with no names but allow docstrings? For all our macros
I think if thereās no names, there should be a way of grouping rules, perhaps.
Itās a shame that Clara Rules doesnāt have anonymous rules. The code makes it look like thatās certainly possible.
(def my-session
(precept/session
(precept/rule [...] => [...])))
That would be a very pretty API
Or maybe:
(def my-session
(precept/session
([...] => [...])))
Is there a reason you suggest embedding rule
inside session
? I'd think that having a defrules
macro might be cleaner. Then you'd pass that symbol created by defrules
to session
's opts.
You could also have grouping with defrules
.
My understanding is that you have rules, and groupings of rules called sessions.
Is there any reason rules couldn't be reused across sessions?
They could, but then youād write:
(def my-rule
(precept/rule [...] => [...]))
(def my-session
(precept/session my-rule ...))
If rules are first-class you could just assign a var to them.
And the linking between sessions and rules would use Clojureās normal require mechanism
So it should be reload-safe.
LGTM. Could have defrule
as a shortcut as well.
Right š
But Clara Rules has some odd complexity that seems unnecessary
(defmacro defrule
[name & body]
(let [doc (if (string? (first body)) (first body) nil)
body (if doc (rest body) body)
properties (if (map? (first body)) (first body) nil)
definition (if properties (rest body) body)
{:keys [lhs rhs]} (dsl/split-lhs-rhs definition)
production (cond-> (dsl/parse-rule* lhs rhs properties {})
name (assoc :name (str (clojure.core/name (com/cljs-ns)) "/" (clojure.core/name name)))
doc (assoc :doc doc))]
(add-production name production)
`(def ~name
~production)))
The def
part in the macro could just be removed
I think š
I think thereās dsl/parse-rule that does that
In clara
Ahh, thanks @alex-dixon
As far as I can tell, the extra stuff in defrule
is to set the docstrings, etc, and to add all the rules to a place where they can be loaded per namespace
And loading per namespace is convenient, butā¦
Kinda makes reloading hard.
I think mk-session accepts a list of āproductionsā that may be rules http://www.clara-rules.org/apidocs/0.15.0/clojure/clara.rules.html#var-mk-session
Like dsl/parse-rule itās CLJ only
Hm. It looks like the rules are only available at compile-time to the Clojure compiler.
;; Store production in cljs.env/*compiler* under ::productions seq?
(defn- add-production [name production]
(swap! env/*compiler* assoc-in [::productions (com/cljs-ns) name] production))
(defn- get-productions-from-namespace
"Returns a map of names to productions in the given namespace."
[namespace]
;; TODO: remove need for ugly eval by changing our quoting strategy.
(let [productions (get-in <@U095WMJNR>/*compiler* [::productions namespace])]
(map eval (vals productions))))
(defn- get-productions
"Return the productions from the source"
[source]
(cond
(symbol? source) (get-productions-from-namespace source)
(coll? source) (seq source)
:else (throw (IllegalArgumentException. "Unknown source value type passed to defsession"))))
Theyāre also doing this for CLJS I think
This is the top of clara/macros.clj
Yeah, so the rules are defined and assembled in Clojureā¦ I think
I think Clojurescriptās eval has matured to the point where that might not be necessary
Including ClojureScriptās eval adds a whole lot of overhead to the compiled JS
Like the entire compiler?
Because it effectively needs to include all of the ClojureScript compiler.
Yeah.
Otherwise Closure just excludes all that.
Hm. Ok
Even with this one? https://cljs.github.io/api/cljs.js/eval
It looks like Clara Rules operates a little like core.match, in that you have a bunch of rules that are evaluated at compile time into Clojure code.
@alex-dixon Pretty sure any eval in your ClojureScript means you need all the compiler stuff.
I guess it would be easy to check
Iāve found Mike Fikes to be an expert in this area
I canāt tell if heās saying Planck can eval Clojurescript in Javascript http://blog.fikesfarm.com/posts/2016-01-22-clojurescript-eval.html
Iām just checking the sizesā¦
79K without, 1.1M with. Assuming Iāve done it right.
Yikes
Thanks for doing that
np
For reference, I compared:
(ns cljseval-test.core
(:require [cljs.js :refer [empty-state eval]]))
(eval (empty-state) '(println "x") identity)
To:
(ns cljseval-test.core)
(println "x")
With eval it was 1.1M, without 79K.
š¢
I think I get what Clara Rules is doing now.
Without its custom registry, youād have to define all the rules together, the same as core.match. Perhaps the authors wanted to leverage namespaces.
I have a few thoughts on the rules setup
!
š
Iām not sure how good these will be. They might be half-baked š
So my understanding of the ClojureScript implementation of Clara Rules is that the rules themselves barely touch ClojureScript. Theyāre defined in Clojure, shoved into the compiler environment, then consumed by the session. They donāt really exist inside of ClojureScript per se.
Because they donāt really map to vars, I wonder if itās worth considering a different registry. When clojure.spec was created, the decision was made not to overload vars and namespaces, and to create a separate registry for specs.
As compile-time artifacts, I wonder if itās worth looking at rules in the same light.
So:
(p/defrule ::todo-is-visible
[[_ :visibility-filter :active]]
[[?e :todo/done false]]
=>
(p/insert! [?e :todo/visible true]))
Using a keyword as the identifier over a symbol would indicate that the rules are not vars in the usual sense. Behind the scenes, perhaps it maps out to something likeā¦
(swap! env/*compiler* assoc-in [::registry ::todo-is-visible]
[:rule '([[_ :visibility-filter :active]]
[[?e :todo/done false]]
=>
(p/insert! [?e :todo/visible true])])])
So just take the data verbatim and stuff it in a registry. Essentially keep all the rules as data until theyāre passed into the session and constructed into an opaque block of code.
Internally Clara Rules might do this - not sure - but Precept adds some additional stuff like subs, so it might be worth keeping everything as compile-time data.
Alternatively:
(swap! env/*compiler* assoc-in [::registry :rules ::todo-is-visible]
'([[_ :visibility-filter :active]]
[[?e :todo/done false]]
=>
(p/insert! [?e :todo/visible true])])])
When creating the session we should be able to do it anonymouslyā¦
From my reading of clara.macros
anyway.
So maybe something like:
(def session
(p/session :rules [:*] :subs [:example.subs/*]))
Could use some kind of basic pattern matching to restrict the rules used in the session.
:*
- any rule, maybe the default
:namespace/*
- any rule in a namespace
:namespace/name
- specific rule
I think it might still be worth identifying rules somehow, because it both documents and provides a degree of uniqueness.
The general idea is to draw a clear dividing line between whatās set up at compile time (the rules registry), and whatās set up at runtime (the session).
This kind of registry has been successful for spec, and in a more similar context re-frame as well. It also seems like this can be implemented without changing clara, which goes some way towards making the rules engine more pluggable?