-
-
Notifications
You must be signed in to change notification settings - Fork 716
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
Ordered Effects #639
Comments
Thanks for putting this together. I vote this :fx form (reg-event-fx
:token
(fn [{:keys [db]} [_ id]]
{:db (assoc db :something id)
:fx [{:dispatch [:blah]} ;; <-- :fx takes a seq of effects
{:dispatch [:other]}
{:http {:url "http://abc.com/endpont :method "GET"}}]})) My argument being
To my mind, facilitating handler composition idea is a huge unlock for re-frame. The current handler calling convention complicates reuse. For example (below) but this is taking it to a logical extreme which might not be justified with such simple handlers. (defn set-something [m]
(assoc-in m [:db :something] (get-in m [:event 1])))
(defn fetch-data [m]
(update-in m [:fx] conj {:http {:url "http://abc.com/endpont" :method "GET"}}))
(defn dispatch-other [m]
(update-in m [:fx] conj {:dispatch [:other]}))
(reg-event-fx2 :token (comp set-something fetch-data dispatch-other)) PS. This discussion provides some insight into the nature of :db. It thinks its useful to recognise it's domain use (state) without getting distracted by the implementation details (cofx + fx). PPS. I'd prefer :fx to expect [{:bah 1} ...] over [:bah 1 ...] for clarity/composability reasons. It's probably a matter of taste but my sense is the CLJ world prefers maps with semantic keys over vectors & conventions. PPPS. I think :fx could be made to simplify the :gofullscreen case (fx with no args) if it's a sticking point. Similarly, dispatch style vectors instead of maps would be compatible and possibly desirable to avoid boilerplate in fx associated with unpacking args. Perhaps the spec for :fx would become... (s/coll (s/or :noop nil?
:fx-0 keyword?
:fx-m map?
:fx-v (s/cat :id keyword? :args (s/* any?)))) PPPPS. Perhaps :db should be registered as a new type of thing allowing for different state models. State being something which passes through handlers. So it's "cofx like" in providing a value to handler and "fx like" in changing app-db on output but specifically associated with one key (e.g. :db) and by convention is passed through by handlers. That seems like a nice way to allow pluggable state management. (reg-state :db
(fn [] @app-db)
(fn [db] (reset! app-db db))) |
Regarding (reg-event-fx
:token
(fn [{:keys [db]} [_ id]]
{:db (assoc db :something id)
:dispatch [:foo] ;; <-- using an "old-style" effect
:fx [:dispatch [:blah]
:dispatch [:other]
:http {:url "http://abc.com/endpont :method "GET"}]})) If If |
Using a I guess a fix for that might be to keep a list of effects which can appear only once and exclude them from consideration in the |
I've let this idea marinate for a few of days and I'm yet to see any downsides. In fact, I'm getting more sure this is a great idea. Even the key I will continue to let it sit for another couple of days just to see if any gremlins or improvements come to mind, before making it happen. While it is only about 5 lines of code, it is all the docs changes which I'm not looking forward to - adding this feature will lead to a change to best practice. I'll have to take on that task incrementally. The benefits:
I'm currently favour this form: (reg-event-fx
:token
(fn [{:keys [db]} [_ id]]
{:db (assoc db :something id)
:fx [{:dispatch [:blah]}
(when seventies? {:hair "side burns"}) ;; nils are ignored
{:http {:method :GET :url "http:/abc.com/endpoint"}}]})) Notes:
Point 4 - Composing Event HandlerPoint 4 of the benefits list (above) is the most intriguing, and the main reason I'm still dwelling on it. If we do this, I want to maximise the value we get from this approach. Now we have this idea in our head, and the smell of it in our nostrils, are there any other tweaks which would add even more value? At a minimum, composing an event handler from smaller functions is now a lot more natural. For example: (reg-event-fx
:token
(fn [{:keys [db]} [_ id]]
(-> {:db db
:fx [{:dispatch [:token2]}]} ;; base structure
(start-ui-throber) ;; <-- might assoc into `:db`
(GET "http://abc.com/endpoint") ;; adds to `:fx`
(POST "http://blah.com/endpoint"))) ;; adds to `:fx`
Notes:
Compare the code above with the existing approach below which tends to encourage "effects via data literals". (reg-event-fx
:token
(fn [{:keys [db]} [_ id]]
{:db (assoc db :twirly true)
:dispatch [:blah]
:http (list
{:method :GET :url "http://abc.com/endpoint" ...}
{:method :POST :url "http://blah.com/endpoint" ...})})) There is something pleasingly explicit to me about maximising the use of data literals, but composing effects via functions is also an interesting approach, and I like the choice, and I like being able to mix the approaches as I see fit. FSM ExampleI am particularly happy because this new approach will make it easier to handle Finite State machines (FSM) within re-frame. I look forward to being able to write: (reg-event-fx
:token
(fn [{:keys [db]} event]
(-> {:db db :fx []} ;; base structure
(fsm fsm-definition event))) ;; process the finite state machine where:
Why
|
But why? If someone doesn't care about the order (I find it to be the case in 90% of the situations I encounter), why suddenly writing
is preferred to a simpler and yet as sufficient
|
@p-himik For one reason, it removes the need for all the Also, it allows for easier composing of effectfulness-causing functions. This is the "Point 4" I talked about above. If you have one function which wants to cause a dispatch, should it be adding to It will become "best practice" because it just gently solves a few sticking points (no one of them a show stopper). And it means it is more natural to "compose effects" (Point 4). And because it is better if there is only one recommended way to do things. |
Indeed. But I still don't really get why it has to be the Yes, {:dispatch-n [[:display-pop-up]
[:start-throbber]
[:fetch-data]
[:start-countdown]]}
;; vs.
{:fx [{:dispatch [:display-pop-up]}
{:dispatch [:start-throbber]}
{:dispatch [:fetch-data]}
{:dispatch [:start-countdown]}} I definitely see such scenario much more often than the need to compose or order the fx. And I personally would never recommend using Actually, writing the above gave me an idea. What if {:fx [[:display-pop-up]
[:start-throbber]
[:fetch-data]
[:start-countdown]]} It as all the benefits of the initially proposed version of |
The docs have to adopt a style. Best for newbies that way.
I detailed the four benefits above. I also pointed out that, to me, the most interesting was "Point 4". I'm confident that it will make it easier to implement certain aspects of FSM. For example, I can see my way clear to finishing this library: https://github.com/day8/re-frame-http-fx-alpha (which stalled because I could never work out a clean way to compose state handlers) But, hey, it is backwards compatible, so no need to change your existing. |
This must be evil, but would be possible: (re-frame/reg-event-fx ::fx
(fn [{:keys [db]} [_ fx]] (fx {:db db :fx []})))
(re-frame/dispatch [::fx (comp set-something fetch-data dispatch-other)]) |
What do you think about using vectors - so having the same structure as used in subs, I think it feels more consistent. Something like this:
|
@ikitommi @kirillsalykin |
Actions:
We'll defer introducing any new |
I imagine it like this:
Basic idea is to use vector literal instead of map literal - so it has same syntax as |
Would it be possible to differentiate old behaviour from new based on type then? Thanks |
Yes, it would be possible to differentiate - if you knew there were two kinds of effects. But existing interceptors expect a map of effects (they don't even know to try and differentiate). They would break.
What you suggest is possible, but I don't see any advantages. At this point, will be going with maps. |
(To answer @kirillsalykin's suggestion) If one day you want to pass some meta data about the effect, the map would have room for that. Maps are future-proof. Fictional example: {:dispatch/retry-on-fail false
:dispatch/may-dispatch-to-service-worker true
:dispatch [:display-pop-up]} |
Yes, that my point on the input side but reg-event-fx does this well enough already. (I don't think :fx order is critical but passing in Not mentioned above, but it's important to me, is that any cofx are passed in/through too. This leads to a sticking point: the return value handling. We want :db and :fx to be handled but since cofx will be passed through (e.g. Here's the current behaviour: (re-frame.core/reg-event-fx ::identity identity)
(re-frame.core/dispatch [::identity])
Console error: re-frame: no handler registered for effect: :event . Ignoring. Questions
Option 1: Document a handler wrapper workaround (defn wrap-handler [f]
(fn [& args]
(-> (apply f args)
(select-keys [:db :fx]))))
Option 2: Move error into :fx (e.g. don't throw on unknown top level keys)
Option 3: New registration option
I vote for Option 2: refactor the error checking in reg-event-fx |
@mike-thompson-day8 @green-coder probably you are right and a map is the way to go. |
@mike-thompson-day8 I've got several remarks, I'll make one comment for each. This first remark only expresses agreement, and provides supporting arguments. 1. Effects are better conj'd than assoc'dI agree with the idea of a sequence of effects in Motivating use case: URL navigationIn an SPA, as a user navigates from a 'page' to another, the effects required are:
|
2. Please do go with tuples, not maps@mike-thompson-day8 I concur that effects under Drawbacks of mapsMaps leave no room for additional annotations on effects; tuples would be more openUse case: As re-frame grows, for example by adding control flow facilities, callers might need to add 'meta-data' to effects, in addition to the key and value that let the
Maybe we can't see clearly right now what we would put in it, but that's all the more reason to leave room for it. Arguably, this very Github issue is of the 'our initial representation was not open enough' kind. (Frankly, if I were re-inventing re-frame from scratch, I would even represent effects as maps with keys Objection A: Effects as 3-tuples? Wouldn't this add arbitrary complexity and ambiguity to their representation?What might worry you: assuming an Actually, I think there's a natural justification for such a representation, which makes for clear guidelines:
Objection B: We can make the
|
I changed my mind, the argument of @vvvvalvalval is valid, triples will be more practical. |
sorry for offtopic, feels like the next step is to use datascript as app-db |
Already doing that by embedding DataScript in |
3. About the
|
The proposal looks good while the precise form should be decided. We are doing something similar already in OrgPad but if this gets implemented, I might reprogram to use the default. I don't really think there is that much value in setting up ordering on effects, as designed they should be independent, so they can be applied in any order. If you really need some order, you should probably combine them into a single effect doing two things sequentially. First, most of event handlers work with a map we called effects which is the input event map. This map is transformed by all steps. In the end, I am using a special clean interceptor to remove keys such as :event to avoid error messages. Further, I programmed most effects to be able to use them multiple times. For instance, :ws effects sends WebSocket messages. Each message is a vector where the first element is id and optional second element is a parameter. :ws effect either gets a single message, or a vector of messages (and sends all of them). To avoid overwritting existing messages, I have a function called ws/add-message which will append the message at the end. |
I think this is off topic but noting here as it relates to the should :fx be a list of maps or vectors which impacts fx handler calling conventions. Most fx handlers involve a callbacks (e.g. on success dispatch x with arg y, on error dispatch z with err). It's always bugged me that there's no convention which encourages "data based" invocation. So you can visually inspect the output of an event handler and know what the callbacks are doing. I tend to pass Clearly this is subject which can be explored separately but establishing a convention for this amounts to constraining how fx handlers are called. Spoiler alert: one arg (which is a map) simplifys things greatly. Vague, half baked idea...
Tradeoffs do-fx wrapper might look like this... (defn dispatch-wrapper [v]
(fn [& args]
(dispatch (into v args))))
(defn dispatchify
[{:keys [resolve reject] :as m}]
(cond-> m
(vector? resolve) (update :resolve dispatch-wrapper)
(vector? reject) (update :reject dispatch-wrapper))) |
Indeed, I think we are all in agreement. That's the essential point here (despite the misleading issue title which talks about ordering).
I find the argument that tuples are easier to process (tests, etc) compelling. I don't find the (future) need for metadata argument that compelling - too speculative. Perhaps that's a lack of imagination on my part. But I was somewhat on the fence anyway. I was nudged towards maps because I don't particularly like the aesthetics of nested vectors, shrug. So your first issue is enough to tip me back towards tuples. So that's now decided unless anyone can unveil a good argument for maps.
I don't feel too strongly either way. On the one hand, I'd prefer that re-frame was permissive and helpful (produces a warning but waves it through) rather than too dictatorial. My assumption is that I'm not clever enough to know all the ways this might be used despite my overly firm opinions on how it SHOULD be used :-) It is a coin flip for me. |
I believe the tuple approach (in which the 2nd element is a map) does not exclude your suggestion for future consideration. |
If a new registration function turns out to be needed then perhaps Entirely documented by the name... an event handler returning :db and :fx where db is processed first. |
@mike-thompson-day8 Here's a random idea, still on the theme of order: you could use the 3rd argument to annotate dependencies between the effects under I don't claim to have good support for legitimizing such an interceptor, but it gives you an idea of the area in which a 3rd argument might play. |
Thinking more about how we could define & differentiate top level :db / :fx handlers and things appearing under :fx key. I think I can see a way to unknot things whichout making a mess. Perhaps we differentiate
This seems to align well with the current code base. We need a way to register effects, to add two common "effect" handlers for :db and :fx, and a new interceptor which replaces do-fx. (defn reg-effect
[id handler]
(register-handler :effect id handler))
(reg-effect :db
(fn [db]
(reset! app-db db)))
(reg-effect :fx
(fn [vs]
(doseq [[fx-key & fx-args :as v] vs :when v]
(if-let [fx-fn (get-handler :fx fx-key)]
(apply fx-fn fx-args)
(console :warn "re-frame: no handler registered for fx:" fx-key ". Ignoring.")))))
(def do-effects
(->interceptor
:id :do-effects
:after (fn do-effects-after
[context]
(trace/with-trace
{:op-type :event/do-effects}
(doseq [[effect-key effect-value] (:effects context)]
(if-let [effect-fn (get-handler :effect effect-key false)]
(effect-fn effect-value)
(console :warn "re-frame: no handler registered for effect:" effect-key ". Ignoring.")))))))
(defn reg-event-dbfx
([id handler]
(reg-event-fx id nil handler))
([id interceptors handler]
(events/register id [cofx/inject-db fx/do-effects std-interceptors/inject-global-interceptors interceptors (fx-handler->interceptor handler)]))) What this effectively does is demote reg-fx. (I see a few options to facilitate backward compatibility but haven't incorporated them in this code) |
@olivergeorge |
We're pushing out a new release containing this change. See change log on v1.1.0 Still to be done:
|
Many Thanks for @olivergeorge for the idea. I think this approach will mature nicely. |
This is awesome! One question: should |
At first I wasn't too sure what the value of the new approach is, for composability (I mean, (Just wanted to add a brief experience report to let you know that this is working out really well for me in real code) |
Okay, time to take the next step: to extract maximum utility from this approach by creating a new As I understand it, the following is effectively what you'd like to see (it currently has too much boilerplate, but let's agree on the approach before trimming down):
So you'll notice this formulation has three steps:
Any new Problems:
Excellent. And thanks for the feedback - good news is always welcome.
Yes, it should. But still getting there. Might be a while. |
@mike-thompson-day8 that reads fine to me. One suggestion, perhaps |
Ahh, using your idea and adding another, I've got it. All problems solved ... The GoalWe want to be able to "compose" an event handler from multiple smaller functions using a new registration function, called maybe (reg-event-dbfx :event-id [function1 function2 function3]) The event handler is a composition of three smaller functions. How?The new Schematically (using the base wrapper (reg-event-ctx ;; <--NOTE: I'm using the most basic wrapper
:event-id
(fn [{:keys [coeffects] :as context}]
(let [initial (assoc coeffects :fx [] :effects-keys [:db :fx] ) ;; <-- step 1
result (-> initial ;; <-- step 2
function1 ;; <-- composing the event handler from smaller functions
function2
function3)
effects (selectkeys result (:effects-keys result)] ;; <-- step 3 - extract the effects
(assoc context :effects effects)))) ;; <-- also step 3 So this formulation has three steps:
Notes:
OutcomeYou'll be able to do this: (reg-event-dbfx ;; <-- new
:event-id
interceptors
[function1 function2 function3]) Just to be clear, function1 might be something like (very simple): (defn start-ui-twirly
[dbfx]
(assoc-in dbfx [:db :twirly-flag] true)) |
Nice. Loving the explicit Not sure if the |
We are about to implement and release |
My only comment is one of naming |
For the record, my earlier schematic using (reg-event-fx
:event-id
(fn [coeffects _]
(let [initial (assoc coeffects :fx []) ;; <-- step 1
result (-> initial ;; <-- step 2
function1 ;; <-- composing the event handler from smaller functions
function2
function3)]
(selectkeys result [:db :fx])))) ;; <-- step 3 - extract and return the effects (which can be written much more tersely) This realisation is encouraging me to wonder if we really need |
I can see the argument in waiting until there's evidence that the boilerplate is annoying or troublesome for third party libs which want to rely on composable behaviour. Some kind of |
Maps are better for long term design - less mental load; easy to compose and manipulate. With ordered fx you force order, even if it is not needed. Forcing order is one of sources of accidental complexity. Order in functional langs are modeled with nesting - instead of switching to vector we can make all events and fxs to be maps and add special keys for chaining. I can imagine collection of high order effects to compose fxs and events. Ideally multiple dispatches can be run in pure world as reduce with merge of fxs. @mike-thompson-day8 let's have a good reasonable complicated use case to exercise design ;) |
About dispatch(-n) - are you sure it is effect at all? What is the reason for event handler to dispatch other events? |
@niquola I, too, at first thought that maps are simpler and compose just as well, but I’ve since switched to this new way and my code really is simpler. The reason is that any function I call simply takes an fx map ( The db update doesn’t change at all, but the effects now become much easier, because if I want to add an effect to be run, I simply add it to the vector. Order usually doesn’t matter so I can just conj it without a second thought (but if order did matter, it’s now an option too). This on first look doesn’t seem any better than just Before, each composed function needed to be aware of the existing effects in the map, or they might clobber them. Eg if you wanted to dispatch you always had to use dispatch-n in case there was already a dispatch effect (or check if it’s there). What if you want to add a http request? You had to check if there already was one in the map to not clobber it. Hopefully there was a http-n effect so you could add a second one. On that note, any time you wrote an effect, you had to provide an -n version, or make all your effects -n aware. This change makes that unnecessary and makes the effects vector explicit about what is being run. Personally, my code got simpler this way because I don’t need to think about what effects have already been added, I just conj to the end of the list. I have a function that sets up a http request and handles a response event. This sets up a number of dispatches and effects and the logic for this got much nicer with this change. If the dbfx sugar functions get added, I will likely be changing all of my handlers to using those. I would suggest trying it out for any complex composed handlers you may have and see for yourself. If you don’t have any, it’s backwards compatible, so you can just stick with the old way. No need to change anything in that case. |
If I had my time over again, I would make
reg-event-fx
return a seq of effects, rather than a map of effects. The resulting effects DSL would be more expressive, at the cost of some nesting.I have previously teased out the issues a bit here:
https://gist.github.com/mike-thompson-day8/1388e68391f4c6c1373c9555c08c7114#using-data-litterals
Advantages:
dispatch-n
, just use:dispatch
multiple times (this same-n
issues comes up a lot with other effects like http GETs etc)Disadvantages:
It might be possible to work around the breaking change problem by introducing a new
reg-event-fx2
which expects the alternatively structured effects return. Allowing us to leave the oldreg-event-fx
alone. But it isn't all sunshine and roses - we'd end up with interceptors which worked with one kind of effects, but not the other. So then there would be v1 interceptors and v2 interceptors. One worked with one kind of effect but not the other.@olivergeorge has made an interesting suggestion which fixes the "breaking change" side of things, at the expense of a tiny bit of boilerplate. He suggests introducing a new
:fx
effect for use withreg-event-fx
which can take seq of other effects.Notice the new
:fx
effect for which aseq
of other effects is provided.Question: if done this way, what is the ordering between
:db
and those in:fx
?:db
first? Probably.Question: I regard
:db
to be a normal effect (it is just a side effect onapp-db
), so it could be put into:fx
too. (What happens if it is not, what is the ordering?)Question: maybe to reduce noise, just pairs like this
At the moment, my inclination is this arrangement (which is slightly hybrid):
Conditional Exclusion
Or if pairs are used ....
As you can see, If paris are used, then It isn't neat to conditionally omit both. Only the
value
part of the pair could be madenil
. This could be a bit clumsy because certain effects actually have a valuenil
value. Eg;:go-fullscreen
The text was updated successfully, but these errors were encountered: