diff --git a/docs/coeffects.md b/docs/coeffects.md index 73edc24..ffed42d 100644 --- a/docs/coeffects.md +++ b/docs/coeffects.md @@ -2,8 +2,8 @@ This tutorial explains `coeffects`. -It explains what they are, how they are created, how they help, and how -to manage them in tests. There's also an adults-only moment. +It explains what they are, how they help, how they can be "injected", and how +to manage them in tests. ## Table Of Contexts @@ -27,16 +27,16 @@ to manage them in tests. There's also an adults-only moment. ### What Are They? -`coeffects` are the input resources that an event handler requires -to perform its computation. By "resources" I mean "data". +`coeffects` are the data resources that an event handler needs +to perform its computation. -Because the majority of event handlers require only `db` and -`event`, there's a registration function, called `reg-event-db`, +Because the majority of event handlers only need `db` and +`event`, there's a specific registration function, called `reg-event-db`, which delivers these two coeffects as arguments to an event -handler, making it easy. +handler, making this common case easy to program. -But sometimes an event handler needs other inputs -to perform their computation. Things like a random number, or a GUID, +But sometimes an event handler needs other data inputs +to perform its computation. Things like a random number, or a GUID, or the current datetime. It might even need access to a DataScript connection. @@ -45,7 +45,7 @@ DataScript connection. This handler obtains data directly from LocalStore ```clj -(reg-event-fx +(reg-event-db :load-defaults (fn [coeffects _] (let [val (js->clj (.getItem js/localStorage "defaults-key"))] ;; <-- Problem @@ -53,148 +53,170 @@ This handler obtains data directly from LocalStore ``` Because it has accessed LocalStore, this event handler is not -pure, which will trigger well documented paper cuts. +pure, and impure functions cause well-documented paper cuts. -### Let's Fix It +### How We Want It -Here's how __we'd like to rewrite that handler__. Data should -only come from the `coeffects` argument: +Our goal in this tutorial is to rewrite this event handler so +that data _only_ comes from the arguments. + +Our first change is to start using `reg-event-fx` (instead of +`reg-event-db`). + +Then we'll seek to have ALL the necessary extra data available in the +first argument, typically called `coeffects`. + +Previous tutorials have show us that we can obtain `:db` from +`coeffects`. Well, not we want it to contain other useful data too. ```clj -(reg-event-fx ;; using -fx registration +(reg-event-fx ;; note: -fx :load-defaults - (fn [coeffects event] ;; 1st argument is coeffects - (let [val (:local-store coeffects) ;; <-- get value from argument - db (:db coeffects)] + (fn [cofx event] ;; cofx means coeffects + (let [val (:local-store cofx) ;; <-- get data from cofx + db (:db cofx)] ;; <-- more data from cofx {:db (assoc db :defaults val))})) ;; returns an effect ``` -Problem solved? Well, yes, the handler is now pure. But we have a -new problem: how do we arrange for the right `:local-store` value -to be available in `coeffects`. +If we can find a way to achieve this, then we are back to +writing pure event handlers. +But what must we do to data into cofx? How do we organise for it +to contain a `:local-store` key, with the right value? + ### How Are Coeffect Babies Made? Well, when two coeffects love each other very much ... no, stop ... this is a G-rated framework. Instead ... -Every time an event handler is executed, a new `context` is created, and within that -`context` is a new `:coeffect` map, which is initially totally empty. +Each time an event handler is executed, a brand new `context` is created, and within that +`context` is a brand new `:coeffect` map, which is initially totally empty. -That pristine `context` value is then threaded through a chain of Interceptors, and sitting -on the end of chain is the event handler, wrapped up as the final interceptor. We know +That pristine `context` value (containing a pristine `:coeffect` map) is then threaded +through a chain of Interceptors before it is finally handled to our event handler +which will be sitting on the end of chain, itself wrapped up in an interceptor. We know this story well from a previous tutorial. -So all members of the Interceptor chain have the opportunity to add to `:coeffects` -via their `:before` function. +So, all members of the Interceptor chain have the opportunity to add to `:coeffects` +via their `:before` function. This is where `:coeffect` gets made. This is where +new keys are added to `:coeffect`, so that later our handler magically finds the +right data in its parameter. ### So, Next Step -Armed with that mindset, let's add an interceptor to the registration, which -puts the right localstore value into coeffect. +If Interceptors put data in `:coeffect`, then we'd better put the right ones on +our handler when we register it. -Here's a sketch: +This handler is the same as before, except for one addition: ```clj (reg-event-fx :load-defaults - [ (coeffect :local-store "defaults-key") ] ;; <-- this is new - (fn [coeffects event] - (let [val (:local-store coeffects) - db (:db coeffects)] + [ (inject-cofx :local-store "defaults-key") ] ;; <-- this is new + (fn [cofx event] + (let [val (:local-store cofx) + db (:db cofx)] {:db (assoc db :defaults val))})) ``` -Problem solved? Well, no, but closer. We're assuming a `coeffects` function. How would it work? +So we've added one Interceptor. It will inject the right value into `context's` `:coeffeects` +and that `:coeffects` ends up being the first parameter to our handler. -### `coeffect` the function +### `inject-cofx` -The `coeffect` function is part of re-frame API. +`inject-cofx` is part of re-frame API. -It returns an Interceptor whose `:before` function loads a value into a `context's` `:coeffect` map. +It is a function which returns an Interceptor whose `:before` function loads +a value into a `context's` `:coeffect` map. + -It takes either one or two arguments. The first is always the `id` of the coeffect -required (called a `cofx-id`). The 2nd is an optional addition value. +`inject-cofx` takes either one or two arguments. The first is always the `id` of the coeffect +required (called a `cofx-id`). The 2nd is an optional addition value. +. -So in the case above, the `cofx-id` was `:local-store` and the additional value was "defaults-key". +So, in the case above, the `cofx-id` was `:local-store` and the additional value +was "defaults-key" which was presumable the place to look in LocalStore -### Other Example Uses of `coeffects` +### More `inject-cofx` Here's some other examples of its use: - - `(coeffects :random-int 10)` - - `(coeffects :guid)` - - `(coeffects :now)` + - `(inject-cofx :random-int 10)` + - `(inject-cofx :guid)` + - `(inject-cofx :now)` So, if I wanted to, I could create an event handler which has access to 3 coeffects: ```clj (reg-event-fx :some-id - [(coeffects :random-int 10) (coeffects :now) (coeffects :local-store "blah")] ;; 3 - (fn [coeffects _] - ... in here I can access coeffect's keys :now :local-store and :random-int)) + [(inject-cofx :random-int 10) (inject-cofx :now) (inject-cofx :local-store "blah")] ;; 3 + (fn [cofx _] + ... in here I can access cofx's keys :now :local-store and :random-int)) ``` -Creating 3 coeffects is probably just showing off, and not generally necessary. +Creating 3 coeffects for the one handler is probably just showing off, and not generally necessary. -And we still have the final piece to put in place. How do we tell this `coeffect` function what to do when -it is given `:now` or `:random-int` ? +And so to the final piece in the puzzle. How does `inject-cofx` know what to do when +it is given `:now` or `:local-store` ? Each `cofx-id` requires a different action. ### Meet `reg-cofx` -This function allows you associate a`cofx-id` (like `:now` or `:local-store`) with a handler function. +This function allows you associate a`cofx-id` (like `:now` or `:local-store`) with a +handler function that you supply. -The handler function registered for a `cofx-id` will be passed a `:coeffects` map, and it -is expected to return a modified map, presumably with an added key and value. +The handler function you register for a given `cofx-id` will be passed two arguments: + - a `:coeffects` map, and + - the optional value supplied +and it is expected to return a modified `:coeffects` map, presumably with an +added key and value. ### Examples -You saw above the use of `:now`. He're is how a `cofx` handler could be registered: +Above we wrote an event handler that wanted `:now` data to be available. Here +is how a handler could be registered for `:now`: ```clj (reg-cofx ;; using this new registration function :now ;; what cofx-id are we registering - (fn [coeffects _] ;; second parameter not used in this case - (assoc coeffects :now (js.Date.)))) ;; add :now key, with value to coeffects + (fn [cofx _] ;; second parameter not used in this case + (assoc cofx :now (js.Date.)))) ;; add :now key, with value ``` And then there's this example: ```clj (reg-cofx ;; new registration function :local-store - (fn [coeffects lsk] ;; second parameter is the local store key + (fn [coeffects local-store-key] (assoc coeffects :local-store - (js->clj (.getItem js/localStorage lsk)))) + (js->clj (.getItem js/localStorage local-store-key)))) ``` -With these two registrations in place, I can now use `(coefect :now)` and -`(coeffect :local-store "blah")` in an effect handler's inteerceptor chain. +With these two registrations in place, I can now use `(inject-cofx :now)` and +`(inject-cofx :local-store "blah")` in an effect handler's interceptor chain. ### The 4 Point Summary -Here is the overall picture, summarised, in note form ... +Here's the overall picture, summarised, in note form ... 1. Event handlers should only source data from their arguments 2. So we have to "inject" required data into coeffect argument - 3. So we use `(coeffects :key)` interceptor in registration - 4. There has to be a coefx handler registered for that `:key` + 3. So we use `(inject-cofx :key)` interceptor in registration of the event handler + 4. There has to be a coefx handler registered for that `:key` (using `reg-cofx`) -XXX should "coeffect" function be called "inject" ... otherwise there just too many different coeffects - ### Secret Interceptors In a previous tutorial we learned that `reg-events-db` and `reg-events-fx` add Interceptors to front of any chain during registration. -We found they inserted an Interceptor called `do-effects`. I can now reveal that -they also add `(coeffect :db)` at the front of each chain. (Last surprise, I promise) +We found they inserted an Interceptor called `do-fx`. I can now reveal that +they also add `(inject-cofx :db)` at the front of each chain. (Last surprise, I promise) -Guess what that adds to coeffects? +Guess what that adds to the `:coeffects` of every event handler? ### Testing -XXX +During testing, you may want to stub out certain diff --git a/src/re_frame/cofx.cljc b/src/re_frame/cofx.cljc index 03fb011..f970fc0 100644 --- a/src/re_frame/cofx.cljc +++ b/src/re_frame/cofx.cljc @@ -12,17 +12,18 @@ (assert (re-frame.registrar/kinds kind)) (def register (partial register-handler kind)) + ;; -- Interceptor ------------------------------------------------------------- -(defn coeffect - "An interceptor which adds to a `context's` `:coeffects`. +(defn inject-cofx + "Returns an interceptor which adds to a `context's` `:coeffects`. `coeffects` are the input resources required by an event handler to perform its job. The two most obvious ones are `db` and `event`. But sometimes a handler might need other resources. Perhaps a handler needs a random number or a GUID or the current datetime. - Perhaps it needs access to an in-memory DataScript database. + Perhaps it needs access to the connection to a DataScript database. If the handler directly access these resources, it stops being as pure. It immedaitely becomes harder to test, etc. @@ -30,29 +31,26 @@ So the necessary resources are \"injected\" into the `coeffect` (map) given the handler. - Given one or more `ids`, this function will iterately lookup the - registered coeffect handlers (via `reg-cofx`) and call each of them - giving the current `:coeffect` as an argument, and expecting a - modified coeffect to be returned. + Given an `id`, and an optional value, lookup the registered coeffect + handler (previously registered via `reg-cofx`) and it with two arguments: + the current value of `:coeffect` and, optionally, the value. The registered handler + is expected to return a modified coeffect. " - ;; Why? We want our handlers to be pure. If a handler calls `js/Date.` then - ;; it stops being as pure. It is harder to test. - ;; - ;; And what if a handler needs a random number? Or a GUID? These kinds of input resources are - ;; termed `coeffects` (sometimes called side-causes). - [& ids] + ([id] (->interceptor :name :coeffects :before (fn coeffects-before [context] - (let [orig-coeffect (:coeffects context) - run-id (fn [coeffect id] - ((get-handler kind id) coeffect)) - new-coeffect (reduce run-id orig-coeffect ids)] - (assoc context :coeffects new-coeffect))))) + (update context :coeffects (get-handler kind id))))) + ([id value] + (->interceptor + :name :coeffects + :before (fn coeffects-before + [context] + (update context :coeffects (get-handler kind id) value))))) -;; -- Standard Builtin CoEffects Handlers -------------------------------------------------------- +;; -- Builtin CoEffects Handlers --------------------------------------------- ;; :db ;; @@ -64,13 +62,17 @@ (assoc coeffects :db @app-db))) -;; this interceptor is so commonly used that we reify it -(def add-db (coeffect :db)) +;; Because this interceptor is used so much, we reify it +(def inject-db (inject-cofx :db)) +(register + :local-store + (fn local-store-handler + [coeffects k] + ()XXXX + (assoc coeffects :db @app-db))) -;; XXX what about a coeffect which reads LocalStore. Use in todomvc example. - -;; -- Example ------------------------------------------------------------------------------------ +;; -- Further Example -------------------------------------------------------- ;; An example coeffect handler, which adds the current datetime under ;; the `:now` key. diff --git a/src/re_frame/core.cljc b/src/re_frame/core.cljc index 384bc09..8da4e28 100644 --- a/src/re_frame/core.cljc +++ b/src/re_frame/core.cljc @@ -55,6 +55,7 @@ ;; -- coeffects (def reg-cofx cofx/register) +(def inject-cofx cofx/inject-cofx) (def clear-cofx (partial registrar/clear-handlers cofx/kind)) @@ -73,21 +74,21 @@ ([id db-handler] (reg-event-db id nil db-handler)) ([id interceptors db-handler] - (events/register id [cofx/add-db fx/do-effects interceptors (db-handler->interceptor db-handler)]))) + (events/register id [cofx/inject-db fx/do-fx interceptors (db-handler->interceptor db-handler)]))) (defn reg-event-fx ([id fx-handler] (reg-event-fx id nil fx-handler)) ([id interceptors fx-handler] - (events/register id [cofx/add-db fx/do-effects interceptors (fx-handler->interceptor fx-handler)]))) + (events/register id [cofx/inject-db fx/do-fx interceptors (fx-handler->interceptor fx-handler)]))) (defn reg-event-ctx ([id handler] (reg-event-ctx id nil handler)) ([id interceptors handler] - (events/register id [cofx/add-db fx/do-effects interceptors (ctx-handler->interceptor handler)]))) + (events/register id [cofx/inject-db fx/do-fx interceptors (ctx-handler->interceptor handler)]))) ;; -- Logging -----