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`.
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

View File

@ -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.

View File

@ -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 -----