precept icon indicating copy to clipboard operation
precept copied to clipboard

Make sessions first-class rather than ambient/global.

Open levand opened this issue 7 years ago • 11 comments

I'm interested in using Precept as the foundation for some of the UI components for some applications I'm working on (including Arachne). I'm fully on board with the declarative rules-based approach, and I have come close to building similar systems a few times in the past (including building Rete implementations on top of Datomic, mirrored by DataScript in the browser.)

However, there are a couple of blockers that currently make it difficult to use Precept as the foundation of other libraries, or as a part of other frameworks:

  1. It's integrated pretty deeply with Reagent (see my comment on #70 )
  2. The sessions (including the "current" session) are stored and referenced globally, rather than being passed as an explicit argument.

It's fine to be opinionated about how apps should be built, but in applications like this those decisions become "contagious" and not all libraries that want to build on Precept want to have the same opinions.

I think Precept is a useful-enough foundational piece that I would be interested in seeing it be as opinion-free as possible, and provide an abstract, highly reusable toolkit for client-side state and event management. Lightweight wrappers could then be built to map that base functionality to specific opinions about application development (global sessions, Reagent, etc).

If this is something you would be willing to consider, I'd be happy to create a fork/PR spiking out some of the refactoring that would be involved for your further consideration.

levand avatar Jun 06 '17 19:06 levand

Great points. I did take an overly opinionated in part because I guess I didn't imagine anyone would want to build upon Precept. 😅

We're certainly amenable to the changes you describe and would greatly appreciate a PR. If it's OK with you I will assign the issue to you and we can go from there.

alex-dixon avatar Jun 06 '17 20:06 alex-dixon

@levand : Thanks for the feedback, and we're hearing you loud and clear on the desire to be as unopinionated as possible.

  1. We can break out the Reagent dependency pretty simply. We are using the Reagent ratom only as one example of bridging from facts in the session to the "view model" maps that component frameworks like React often require. We are now looking into using the current Reagent functions as the default lightweight wrapper (so it works out of the box) but allow you to override with your own "sync component props" functions and do whatever you'd like.

  2. We agree in general about avoiding ambient globals -- looking forward to your PR to see what you have in mind. Do you have a specific use case for maintaining your own sessions? We're curious if it matches up with some of our future ideas, like maintaining different sessions for completely separate app contexts.

mikegai avatar Jun 06 '17 22:06 mikegai

@mikegai IMHO, collections of state (like sessions) should be wrapped up into a single value, and passed around explicitly as an extra argument wherever they're needed, keeping functions pure (in the technical sense.)

Sure, the downside is that it's some extra bookkeeping. The upside is that your library can be consumed and reused by libraries that have committed to exposing only pure functions. And of course you can always go back and stuff the session in a global atom (in a wrapper lib) if you really want to use it that way.

levand avatar Jun 06 '17 23:06 levand

So, I'm sorry to say that I spent a fair amount of time on this, but am not going to attempt to complete it.

I still think it would be valuable, but after working with the code, to do it "right" would basically be equivalent to a full rewrite, not only changing the API but touching almost every line of code.

Changes of this scope should be handled (if at all) by a project maintainer to ensure overall consistency of the projects vision: I'm not sure I could do it without "hijacking" the project, which I have no desire to do.

I may still do a bit of hacking on a fresh approach to determine what a purely functional API for this kind of thing would be, and I'm happy to share learnings back and forth, but I won't be working on a PR for this any more.

Feel free to close this issue or leave it open as a future "nice to have" in case you end up doing a rewrite/refactoring at some point anyway.

Thanks!

levand avatar Jun 11 '17 17:06 levand

@levand Thanks for your honesty! We were thinking it was something we should take on as well, but weren't about to turn away a such a willing and able contributor as yourself.

We greatly appreciate your efforts and have no problem at all trying to move forward and pick it up from here.

Leaving the issue open as this is our top priority issue.

alex-dixon avatar Jun 11 '17 22:06 alex-dixon

Anyone interested in an implementation of this design should definitely check out @levand's https://github.com/arachne-framework/factui.

So far we have not faced any major limitations managing a session as a single global state atom. It is a blocker for managing multiple sessions though, so this remains a a high priority feature for us. That said "number one priority" turned out to be an overstatement and I apologize for that and this isn't something we're working toward currently.

I'm not able to think of other use cases this would enable at the moment. It'd definitely help me with prioritizing if anyone would like to weigh in.

alex-dixon avatar Aug 06 '17 17:08 alex-dixon

Would it be possible to work around the global session issue by loading a ClojureScript namespace at runtime in a fresh ClojureScript context? So that rules are loaded in a given context that can be mutated or recycled.

theronic avatar Feb 14 '18 12:02 theronic

I think that’s a brilliant idea.

It would seem to require the cljs compiler. In dev mode I think Precept already does this.

If you can explain more about your goals with this/use case I may be able to propose or suggest alternatives.

alex-dixon avatar Feb 14 '18 20:02 alex-dixon

In trying to learn more about how precept works ended up building this: https://github.com/clyfe/clara-eav Some thoughts:

  1. May be of interest to carry over the session wrapper that attaches state to sessions, and do away with some globals.
  2. I find parsing and compiling macros [e a v] sugar via clojue.spec is more manageable.
  3. [[x]] fact sugar should match by eid in all datoms, rather than attribute/type; the latter is rarely useful.
  4. Newcomers would grasp precept faster if the API surface would be reduced. Say, "prolog style rules" could be removed.
  5. Why are the entity/entities/<- macros needed? Can't that be done via accumulators? Maybe I'm missing something.

Thanks all for all the effort on precept. Haven't used it yet, but it's all quite interesting.

clyfe avatar Aug 18 '18 11:08 clyfe

@clyfe Awesome. I'd recommend sharing clara-eav on Clara's Slack channel on Clojurians since I see a fair number of people there looking to use eav syntax with it.

May be of interest to carry over the session wrapper that attaches state to sessions, and do away with some globals.

I'm currently working on a Clojure-only fork of Precept that allows this.

Overall I've come to terms with global state in the browser. It's no different from re-frame. All the globals are public. In practice it's only an issue when there's more than one "app" or you want to manage multiple sessions. Maybe the number of facts becomes so large that you want to partition by route. I haven't run into a problem with that though, and splitting into multiple sessions presents additional challenges that I'd want to deal with at the framework level.

It can be a problem for Clojure and server environments where it's more natural to want more than one session in the same process, since the auxiliary indexes for schema maintenance need to travel with a session. The fork I'm working on addresses that using the same basic approach as clara-eav's SessionWrapper. That said, we've used Precept in Clojure in production and that wasn't actually needed.

I'd need to know what specific use cases and problems people are running into before making changes to Precept to accommodate them.

I find parsing and compiling macros [e a v] sugar via clojue.spec is more manageable.

Totally. I've used this in fork I'm working on. I'd like to port it back here but I got rid of the double vectors for positional tuple matches and Precept depends heavily on that.

[[x]] fact sugar should match by eid in all datoms, rather than attribute/type; the latter is rarely useful.

Not sure I follow completely but Clara requires a fact type. We get around that by using Clojure's hierarchy so that all attributes descend from a single ancestor (currently :all) to allow matching on eid or value only.

Newcomers would grasp precept faster if the API surface would be reduced. Say, "prolog style rules" could be removed.

This is good feedback. We could try to move them out of the readme and examples. My main issue with them is that it's not clear how to insert a println statement like you can with rules.

Why are the entity/entities/<- macros needed? Can't that be done via accumulators? Maybe I'm missing something.

They're optional. I think they're helpful but also symptomatic of a bigger problem, that we can't currently use rules to talk about an accumulated set of facts without inserting the accumulated results into the session. Unlike Datomic, rules and queries return one match at a time. So right now you can't do something like this, where many conditions are specified and a list of everything matching it is returned:

(d/q '[:find ?sku
       :in $ ?inv
       :where [?item :item/id ?inv]
              [?order :order/items ?item]
              [?order :order/items ?other-item]
              [?other-item :item/id ?other-inv]
              [?other-inv :inv/sku ?sku]]
     db [:inv/sku "SKU-25"])
=> [["SKU-25"] ["SKU-26"]]

We'd like to have a facility for that, not this syntax but something like:

?important-todos <- (acc/all)  :from [?e :todo/done false]
                          [?e :todo/title (.startsWith ?v "important")]

instead of

rule1
[?e :todo/title (.startsWith ?v "important")]
=>
(insert! [?e :todo/important? true]))

subscription-helper-rule
[?e :todo/important? true]
[?e :todo/done false]
[?important-todo <- (acc/all) :from [?e :all _]]
=>
(insert [(uuid) :important-todo/todo ?important-todo)

subscription/all-important-todos
[?important-todos <- (acc/all :v) :from [_ :important-todo/todo _]
=>
{:data ?important-todos}

Hopefully that's not too contrived.

alex-dixon avatar Aug 19 '18 17:08 alex-dixon

Not sure I follow completely but Clara requires a fact type.

I meant "attribute only" fact should rather be "eid only": [[:foo]] should resolve to [:all (= (:e this) :foo)] instead [:foo].

Unlike Datomic, rules and queries return one match at a time.

Just rules are incremental, not queries. Queries are total. AFAICT at least.

Hopefully that's not too contrived.

Thanks for the example, it brings some insight.

clyfe avatar Aug 19 '18 18:08 clyfe