-
-
Notifications
You must be signed in to change notification settings - Fork 715
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Derived Values #680
Comments
I wouldn't constrain the time, i.e. I think that the 2nd, the 3rd, and the last points should not be there. Calling I don't think there's any extra mechanism that's needed - I think
On a related note, recently you mentioned that you were looking at rule engines. I'm still not entirely sold on the idea of them being useful for |
I believe the requirement is often time-constrained (but certainly not always). So, I believe that's an essential part of the problem statement because it will make some solutions viable and others not so.
I know you know this, but for clarity, I'll point out that this statement isn't true. So this comes back to time again. Or, better, "state". Sometimes the application is in a state which requires these materialised views, and sometimes it isn't. At the moment, the state is implicit. The existence of certain Views (or not) is what drives subscriptions which, in turn, drives nodes in the Signal graph, which does the calculation of "derived data". I know you know all this. But I'm trying to be crystal clear. A good problem definition is the best gift you can give yourself. So my summary: applications have different states. In different states they need to calculate certain "derived values" and those values might need to be delivered to views or might be used within event handlers. Subscriptions are certainly a very nice way of handling this because they come into existence implicity when "the state" (of the UI) asks for them, and then disappear when no longer being used (by views). app-db --> views (state of the UI) --> subscriptions (state of Signal Graph) --> computation (derived state) --> views and event handlers |
We're talking about different kinds of graphs. I was thinking specifically about what you call the template, and you're describing the concrete values graph. To the former, the views don't matter at all. Same with events - they don't care about views, they just care about cofx. It doesn't matter if a particular view is mounted and is the one that requested value X if some completely unrelated event that has nothing to do with that view or even the whole UI really needs that value X. To me, it's easy to imagine a situation where a re-frame app is run without any views at all. Only events that use that subscriptions template to create some concrete values and reuse them when possible, thus creating a cached value graph.
I might be reading it wrong but these sentences feel like there's a direction of intent, so to speak. The state changes (i.e. app-db), and then stuff happens because of that change, including some event handlers potentially asking some subs for their values. |
Here’s my attempt at what the needs are. Got a bit lenghty… First of all I feel like I understand the differentiation between template vs graph. The former is a blueprint representing how it will be connected and calculated if ever needed and the latter represents what is actually in use with caching and recalculations on change. I also see that this is “simple” for UI registered subscriptions as they explicitly send creation and teardown signals. For event subs the template part would work exactly the same, and while dispatch of an event could itself be seen as a creation signal, there is no explicit teardown (unless you do it right away like re-frame-utils). This last part makes it tricky, more inefficient and/or potentially more involved for the end user if you want to provide more control over this behavior (one-off vs TTL vs permanent). From a usability point-of-view I would say the ideal solution to me is something like this:
The “extreme” for 1) is e.g. a js/setInterval dispatching an event that depends on a subscription chain that has nothing in comon with any UI subscription needs. I think I could personally live with quite a few limitations, as long as what you run into is performance penalties and not malfunction. One usage scenario I have: The above is a description of sibling subscriptions where both the UI and event sub have common ancestors in the graph. Likely at least one level 3 subscription in common, but after that the needs deviate. Subscription used in event is as described above while the UI might need eg. translation of key names to something acceptable for interop with d3js (e.g. no namespaced keys and underscore instead of dashes in names). An attempt at describing the above sibling approach in re-frame like psudo code:
As mentioned on slack, another usecase/benefit I see is to hide implementation details by storing data in db under a auto namespaced key (e.g: ::raw-entities), and provide a subscription where the rest of the code can get hold of this data (<sub [:entities/raw]). Features wise, I don't see that that usecase has any more requirements than already mentioned. The key is no dependency on UI usage. As I don’t know the re-frame codebase I should not venture into implementation ideas, but for what it’s worth: I hope it helps. If not, feel free to direct input needs in the right direction. |
Is there any progress? The more our re-frame application grows the more is the need to reuse subscriptions inside the events. We have a lot of derived values and duplicating subscriptions logic to be able to use it inside events becomes harder and harder. Even the solution that does not cache the calculation but runs it every time would be a huge step forward for us because at the moment we do those calculations anyway or have to pass a lot of data through the UI even if we don't need it in the UI itself. Honestly, most of the team members prefer the latter approach because they don't have to refactor all the subscriptions involved. E.g.
|
I think most of this could be resolved, if when discarding a reaction after it's removed from the subscription cache it'd instead be moved to a small secondary LRU cache. That way the most commonly used event-only subscriptions would still stay cached. |
Here's my take on things, and a prototype: 7055a38 I think I know why this issue is unsolved for 8 years. Caching means trading state for computation. Concretely, ram for flops. We call a function unreasonable when it has different effects at different times & places. #754 doesn't help. It just changes the shadow-API. There are more solutions out there. They're all bound to be incomplete. What if you dispose a sub, just to bring it back 1ms later? What if your sub has a big memory footprint, but you only need it once? So, there's no single way a sub should work. In other words, subscriptions are polymorphic. Clojure has its cake and eats it.
We don't avoid complex behavior, but at least now we've named it.
We use metadata to colocate lifecycle and query (best-effort). Thanks for coming to my ted talk. |
I made a second prototype: 1238515 I think breaking the API at a different point makes it simpler overall. Query-maps are simpler. No more looking up the "first" registered key to find lifecycle and id. A query-vector is used as-is. No more putting a query-map inside the query-vector.
Sub handlers always know the lifecycle of the query. No more exceptions. Registering a lifecycle is simpler. No more arities. Now it can handle map or vector queries naively. |
My polymorphic subscription prototype is still in alpha, but we've been working the new |
I've updated a few articles to explain more clearly the problem with subscriptions, and why they're a leaky abstraction of dataflow programming. |
Upgrade re-frame to latest, from v1.3.0 (released on 2022-08-27) to latest v1.4.3 (released on 2024-01-25). Important changes: - [Added] re-frame.alpha namespace, for testing proposed features (see flows (day8/re-frame#795) and polymorphic subscriptions day8/re-frame#680 (comment)). - [Added] dispatch-sync now emits a :sync trace to indicate when it has finished. - Re-frame upgraded its dependency on Reagent to latest v1.2.0. - There are two breaking changes in v1.4.0, but they don't affect us because we don't use interceptors path and unwrap.
My initial cut of the problem definition is as follows:
Currently, subscriptions provide a way to create "derived values" that compute only when needed. So they tend to be the tool we reach for. But is there a better solution??
The text was updated successfully, but these errors were encountered: