The function coeffect is renamed to inject-cofx.

When writing the docs I found that sentences had too many mentions of `coeffects` with different meanings, which was confusing.  With this change, I remove one possible meaning of `coeffect` and, in the process I end up with a clearer name anyway.
This commit is contained in:
Mike Thompson 2016-08-16 12:30:01 +10:00
parent 1efcea2cf2
commit 4f2f772afd
3 changed files with 122 additions and 97 deletions

View File

@ -2,8 +2,8 @@
This tutorial explains `coeffects`. This tutorial explains `coeffects`.
It explains what they are, how they are created, how they help, and how It explains what they are, how they help, how they can be "injected", and how
to manage them in tests. There's also an adults-only moment. to manage them in tests.
## Table Of Contexts ## Table Of Contexts
@ -27,16 +27,16 @@ to manage them in tests. There's also an adults-only moment.
### What Are They? ### What Are They?
`coeffects` are the input resources that an event handler requires `coeffects` are the data resources that an event handler needs
to perform its computation. By "resources" I mean "data". to perform its computation.
Because the majority of event handlers require only `db` and Because the majority of event handlers only need `db` and
`event`, there's a registration function, called `reg-event-db`, `event`, there's a specific registration function, called `reg-event-db`,
which delivers these two coeffects as arguments to an event 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 But sometimes an event handler needs other data inputs
to perform their computation. Things like a random number, or a GUID, to perform its computation. Things like a random number, or a GUID,
or the current datetime. It might even need access to a or the current datetime. It might even need access to a
DataScript connection. DataScript connection.
@ -45,7 +45,7 @@ DataScript connection.
This handler obtains data directly from LocalStore This handler obtains data directly from LocalStore
```clj ```clj
(reg-event-fx (reg-event-db
:load-defaults :load-defaults
(fn [coeffects _] (fn [coeffects _]
(let [val (js->clj (.getItem js/localStorage "defaults-key"))] ;; <-- Problem (let [val (js->clj (.getItem js/localStorage "defaults-key"))] ;; <-- Problem
@ -53,133 +53,155 @@ This handler obtains data directly from LocalStore
``` ```
Because it has accessed LocalStore, this event handler is not 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 Our goal in this tutorial is to rewrite this event handler so
only come from the `coeffects` argument: 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 ```clj
(reg-event-fx ;; using -fx registration (reg-event-fx ;; note: -fx
:load-defaults :load-defaults
(fn [coeffects event] ;; 1st argument is coeffects (fn [cofx event] ;; cofx means coeffects
(let [val (:local-store coeffects) ;; <-- get value from argument (let [val (:local-store cofx) ;; <-- get data from cofx
db (:db coeffects)] db (:db cofx)] ;; <-- more data from cofx
{:db (assoc db :defaults val))})) ;; returns an effect {:db (assoc db :defaults val))})) ;; returns an effect
``` ```
Problem solved? Well, yes, the handler is now pure. But we have a If we can find a way to achieve this, then we are back to
new problem: how do we arrange for the right `:local-store` value writing pure event handlers.
to be available in `coeffects`.
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? ### How Are Coeffect Babies Made?
Well, when two coeffects love each other very much ... no, stop ... this Well, when two coeffects love each other very much ... no, stop ... this
is a G-rated framework. Instead ... is a G-rated framework. Instead ...
Every time an event handler is executed, a new `context` is created, and within that Each time an event handler is executed, a brand new `context` is created, and within that
`context` is a new `:coeffect` map, which is initially totally empty. `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 That pristine `context` value (containing a pristine `:coeffect` map) is then threaded
on the end of chain is the event handler, wrapped up as the final interceptor. We know 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. this story well from a previous tutorial.
So all members of the Interceptor chain have the opportunity to add to `:coeffects` So, all members of the Interceptor chain have the opportunity to add to `:coeffects`
via their `:before` function. 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 ### So, Next Step
Armed with that mindset, let's add an interceptor to the registration, which If Interceptors put data in `:coeffect`, then we'd better put the right ones on
puts the right localstore value into coeffect. our handler when we register it.
Here's a sketch: This handler is the same as before, except for one addition:
```clj ```clj
(reg-event-fx (reg-event-fx
:load-defaults :load-defaults
[ (coeffect :local-store "defaults-key") ] ;; <-- this is new [ (inject-cofx :local-store "defaults-key") ] ;; <-- this is new
(fn [coeffects event] (fn [cofx event]
(let [val (:local-store coeffects) (let [val (:local-store cofx)
db (:db coeffects)] db (:db cofx)]
{:db (assoc db :defaults val))})) {: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
`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. 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: Here's some other examples of its use:
- `(coeffects :random-int 10)` - `(inject-cofx :random-int 10)`
- `(coeffects :guid)` - `(inject-cofx :guid)`
- `(coeffects :now)` - `(inject-cofx :now)`
So, if I wanted to, I could create an event handler which has access to 3 coeffects: So, if I wanted to, I could create an event handler which has access to 3 coeffects:
```clj ```clj
(reg-event-fx (reg-event-fx
:some-id :some-id
[(coeffects :random-int 10) (coeffects :now) (coeffects :local-store "blah")] ;; 3 [(inject-cofx :random-int 10) (inject-cofx :now) (inject-cofx :local-store "blah")] ;; 3
(fn [coeffects _] (fn [cofx _]
... in here I can access coeffect's keys :now :local-store and :random-int)) ... 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 And so to the final piece in the puzzle. How does `inject-cofx` know what to do when
it is given `:now` or `:random-int` ? it is given `:now` or `:local-store` ? Each `cofx-id` requires a different action.
### Meet `reg-cofx` ### 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 The handler function you register for a given `cofx-id` will be passed two arguments:
is expected to return a modified map, presumably with an added key and value. - 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 ### 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 ```clj
(reg-cofx ;; using this new registration function (reg-cofx ;; using this new registration function
:now ;; what cofx-id are we registering :now ;; what cofx-id are we registering
(fn [coeffects _] ;; second parameter not used in this case (fn [cofx _] ;; second parameter not used in this case
(assoc coeffects :now (js.Date.)))) ;; add :now key, with value to coeffects (assoc cofx :now (js.Date.)))) ;; add :now key, with value
``` ```
And then there's this example: And then there's this example:
```clj ```clj
(reg-cofx ;; new registration function (reg-cofx ;; new registration function
:local-store :local-store
(fn [coeffects lsk] ;; second parameter is the local store key (fn [coeffects local-store-key]
(assoc coeffects (assoc coeffects
:local-store :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 With these two registrations in place, I can now use `(inject-cofx :now)` and
`(coeffect :local-store "blah")` in an effect handler's inteerceptor chain. `(inject-cofx :local-store "blah")` in an effect handler's interceptor chain.
### The 4 Point Summary ### 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 1. Event handlers should only source data from their arguments
2. So we have to "inject" required data into coeffect argument 2. So we have to "inject" required data into coeffect argument
3. So we use `(coeffects :key)` interceptor in registration 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` 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 ### Secret Interceptors
@ -187,14 +209,14 @@ XXX should "coeffect" function be called "inject" ... otherwise there just too
In a previous tutorial we learned that `reg-events-db` In a previous tutorial we learned that `reg-events-db`
and `reg-events-fx` add Interceptors to front of any chain during registration. 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 We found they inserted an Interceptor called `do-fx`. I can now reveal that
they also add `(coeffect :db)` at the front of each chain. (Last surprise, I promise) 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 ### Testing
XXX During testing, you may want to stub out certain

View File

@ -12,17 +12,18 @@
(assert (re-frame.registrar/kinds kind)) (assert (re-frame.registrar/kinds kind))
(def register (partial register-handler kind)) (def register (partial register-handler kind))
;; -- Interceptor ------------------------------------------------------------- ;; -- Interceptor -------------------------------------------------------------
(defn coeffect (defn inject-cofx
"An interceptor which adds to a `context's` `:coeffects`. "Returns an interceptor which adds to a `context's` `:coeffects`.
`coeffects` are the input resources required by an event handler `coeffects` are the input resources required by an event handler
to perform its job. The two most obvious ones are `db` and `event`. to perform its job. The two most obvious ones are `db` and `event`.
But sometimes a handler might need other resources. But sometimes a handler might need other resources.
Perhaps a handler needs a random number or a GUID or the current datetime. 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 If the handler directly access these resources, it stops being as
pure. It immedaitely becomes harder to test, etc. pure. It immedaitely becomes harder to test, etc.
@ -30,29 +31,26 @@
So the necessary resources are \"injected\" into the `coeffect` (map) So the necessary resources are \"injected\" into the `coeffect` (map)
given the handler. given the handler.
Given one or more `ids`, this function will iterately lookup the Given an `id`, and an optional value, lookup the registered coeffect
registered coeffect handlers (via `reg-cofx`) and call each of them handler (previously registered via `reg-cofx`) and it with two arguments:
giving the current `:coeffect` as an argument, and expecting a the current value of `:coeffect` and, optionally, the value. The registered handler
modified coeffect to be returned. is expected to return a modified coeffect.
" "
;; Why? We want our handlers to be pure. If a handler calls `js/Date.` then ([id]
;; 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]
(->interceptor (->interceptor
:name :coeffects :name :coeffects
:before (fn coeffects-before :before (fn coeffects-before
[context] [context]
(let [orig-coeffect (:coeffects context) (update context :coeffects (get-handler kind id)))))
run-id (fn [coeffect id] ([id value]
((get-handler kind id) coeffect)) (->interceptor
new-coeffect (reduce run-id orig-coeffect ids)] :name :coeffects
(assoc context :coeffects new-coeffect))))) :before (fn coeffects-before
[context]
(update context :coeffects (get-handler kind id) value)))))
;; -- Standard Builtin CoEffects Handlers -------------------------------------------------------- ;; -- Builtin CoEffects Handlers ---------------------------------------------
;; :db ;; :db
;; ;;
@ -64,13 +62,17 @@
(assoc coeffects :db @app-db))) (assoc coeffects :db @app-db)))
;; this interceptor is so commonly used that we reify it ;; Because this interceptor is used so much, we reify it
(def add-db (coeffect :db)) (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. ;; -- Further Example --------------------------------------------------------
;; -- Example ------------------------------------------------------------------------------------
;; An example coeffect handler, which adds the current datetime under ;; An example coeffect handler, which adds the current datetime under
;; the `:now` key. ;; the `:now` key.

View File

@ -55,6 +55,7 @@
;; -- coeffects ;; -- coeffects
(def reg-cofx cofx/register) (def reg-cofx cofx/register)
(def inject-cofx cofx/inject-cofx)
(def clear-cofx (partial registrar/clear-handlers cofx/kind)) (def clear-cofx (partial registrar/clear-handlers cofx/kind))
@ -73,21 +74,21 @@
([id db-handler] ([id db-handler]
(reg-event-db id nil db-handler)) (reg-event-db id nil db-handler))
([id interceptors 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 (defn reg-event-fx
([id fx-handler] ([id fx-handler]
(reg-event-fx id nil fx-handler)) (reg-event-fx id nil fx-handler))
([id interceptors 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 (defn reg-event-ctx
([id handler] ([id handler]
(reg-event-ctx id nil handler)) (reg-event-ctx id nil handler))
([id interceptors 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 ----- ;; -- Logging -----