re-frame/docs/Interceptors.md

363 lines
12 KiB
Markdown
Raw Normal View History

2016-08-13 06:34:25 +00:00
## Introduction
2016-08-15 01:56:03 +00:00
This is an interceptors tutorial.
2016-08-13 06:34:25 +00:00
## Table Of Contents
- [Introduction](#introduction)
- [Table Of Contents](#table-of-contents)
- [Interceptors](#interceptors)
* [Why Interceptors?](#why-interceptors-)
* [What Do Interceptors Do?](#what-do-interceptors-do-)
* [Wait, I know That Pattern!](#wait--i-know-that-pattern-)
* [What's In The Pipeline?](#what-s-in-the-pipeline-)
* [Show Me](#show-me)
* [Handlers Are Interceptors Too](#handlers-are-interceptors-too)
- [Executing A Chain](#executing-a-chain)
* [The Links](#the-links)
* [What Is Context?](#what-is-context-)
* [Self Modifing](#self-modifing)
* [Credit](#credit)
* [Write An Interceptor](#write-an-interceptor)
* [Wrapping Handlers](#wrapping-handlers)
- [Summary](#summary)
- [Appendix](#appendix)
* [The Builtin Interceptors](#the-builtin-interceptors)
## Interceptors
2016-08-11 04:33:28 +00:00
### Why Interceptors?
2016-08-11 06:09:43 +00:00
Two reasons.
2016-08-11 13:24:39 +00:00
__First__, we want simple event handlers.
2016-08-11 04:33:28 +00:00
Interceptors can look after "cross-cutting" concerns like undo, tracing and validation.
They help us to factor out commonality, hide complexity and introduce further steps into the "Derived Data,
Flowing" story promoted by re-frame.
So, you'll want to use Interceptors - they're helpful.
2016-08-11 13:24:39 +00:00
__Second__, under the covers, Interceptors are the means by which
event handlers are executed (when you `dispatch`). You'll
want to understand how that happens.
2016-08-11 04:33:28 +00:00
2016-08-11 13:24:39 +00:00
### What Do Interceptors Do?
2016-08-11 04:33:28 +00:00
They wrap.
Specifically, they wrap event handlers.
2016-08-13 06:34:25 +00:00
Imagine your event handler is like a piece of ham. An interceptor would be
2016-08-11 04:33:28 +00:00
like bread either side of your ham, which makes a sandwich.
And two Interceptors, in a chain, would be like you put another
pair of bread slices around the outside of the existing sandwich to make
a sandwich of the sandwich. Now it is a very thick sandwich.
Interceptors wrap on both sides of a handler, layer after layer.
### Wait, I know That Pattern!
Interceptors implement middleware. But differently.
2016-08-11 13:24:39 +00:00
Traditional middleware - often seen in web servers - creates a data
2016-08-11 04:33:28 +00:00
processing pipeline via the nested composition of higher order functions.
The result is a "stack" of functions. Data flows through this pipeline,
first forwards from one end to the other, and then backwards.
Interceptors achieve the same outcome by assembling functions, as data,
in a collection (a chain, rather than a stack). Data can then be iteratively
pipelined, first forwards through the functions in the chain,
and then backwards along the same chain.
Because the interceptor pipeline is composed via data, rather than
higher order functions, it is a more flexible arrangement.
### What's In The Pipeline?
2016-08-11 13:24:39 +00:00
Data. It flows through the pipeline being progressively transformed.
2016-08-11 04:33:28 +00:00
Fine. But what data?
With a web server, the middleware "stack" progressively
transforms a `request` in one direction, and, then in the backwards
sweep, it progressively produces a `response`.
In re-frame, the forwards sweep progressively creates the `coeffects`
(inputs to the handler), while the backwards sweep processes the `effects`
(outputs from the handler).
I'll pause while you read that sentence again. That's the key
concept, right there.
2016-08-11 06:09:43 +00:00
### Show Me
2016-08-11 04:33:28 +00:00
You can provide a chain of interceptors when
2016-08-11 13:24:39 +00:00
you register an event handler.
2016-08-11 04:33:28 +00:00
Using a 3-arity registration function:
```clj
(reg-event-db
:some-id
[in1 in2] ;; <--- a chain of 2 interceptors
2016-08-11 06:09:43 +00:00
(fn [db v] ;; <-- the handler here, as before
2016-08-11 04:33:28 +00:00
....)))
```
2016-08-13 06:34:25 +00:00
> Each Event Handler can have its own tailored interceptor chain, provided at registration-time.
2016-08-11 04:33:28 +00:00
2016-08-11 13:24:39 +00:00
### Handlers Are Interceptors Too
2016-08-11 04:33:28 +00:00
2016-08-11 06:09:43 +00:00
You might see that registration above as associating `:some-id` with two things: (1) a chain of interceptors
2016-08-11 04:33:28 +00:00
and (2) a handler.
2016-08-11 06:09:43 +00:00
Except, the handler is turned into an interceptor too. (We'll see how later)
2016-08-11 04:33:28 +00:00
2016-08-11 06:09:43 +00:00
So `:some-id` is only associated with one thing: a 3-chain of interceptors,
2016-08-11 04:33:28 +00:00
with the handler wrapped in an interceptor and put on the end of the other two.
2016-08-13 06:34:25 +00:00
Except, the registration function itself, `reg-event-db`, actually takes this 3-chain
and inserts its own interceptors
2016-08-11 04:33:28 +00:00
(which do useful things) at the front (more on this soon too),
2016-08-13 06:34:25 +00:00
so ACTUALLY, there's about 5 interceptors in the chain.
2016-08-11 04:33:28 +00:00
2016-08-13 06:34:25 +00:00
So, ultimately, that event registration associates the event id `some-id`
with a chain of interceptors.
2016-08-11 06:09:43 +00:00
Later, when a `dispatch` for `:some-id` is done, that 5-chain of
2016-08-13 06:34:25 +00:00
interceptors will be "executed". And that's how events get handled.
2016-08-11 04:33:28 +00:00
2016-08-13 06:34:25 +00:00
## Executing A Chain
### The Links
2016-08-11 04:33:28 +00:00
Each interceptor has this form:
```clj
2016-08-11 06:09:43 +00:00
{:id :something ;; decorative only
2016-08-11 04:33:28 +00:00
:before (fn [context] ...) ;; returns possibly modified context
:after (fn [context] ...)} ;; `identity` would be a noop
```
That's essentially a map of two functions. Now imagine a vector of these maps - that's an
2016-08-13 06:34:25 +00:00
an interceptor chain.
2016-08-11 04:33:28 +00:00
2016-08-11 06:09:43 +00:00
To "execute" an interceptor chain:
2016-08-11 04:33:28 +00:00
1. create a `context` (a map, described below)
2016-08-11 06:09:43 +00:00
2. iterate forwards over the chain, calling the `:before` function on each interceptor
3. iterate over the chain in the opposite direction calling the `:after` function on each interceptor
2016-08-11 04:33:28 +00:00
Remember that the last interceptor in the chain is the handler itself (wrapped up to be the `:before`).
2016-08-11 06:09:43 +00:00
That's it. That's how an event gets handled.
### What Is Context?
2016-08-11 04:33:28 +00:00
2016-08-11 06:09:43 +00:00
Some data called a `context` is threaded through all the calls.
2016-08-11 13:24:39 +00:00
This value is passed as the argument to every `:before` and `:after`
function and they returned it, possibly modified.
2016-08-11 04:33:28 +00:00
2016-08-11 13:24:39 +00:00
A `context` is a map with this structure:
2016-08-11 04:33:28 +00:00
```clj
2016-08-11 06:09:43 +00:00
{:coeffects {:event [:some-id :some-param]
2016-08-11 04:33:28 +00:00
:db <original contents of app-db>}
2016-08-11 06:09:43 +00:00
2016-08-11 04:33:28 +00:00
:effects {:db <new value for app-db>
:dispatch [:an-event-id :param1]}
2016-08-11 06:09:43 +00:00
2016-08-11 04:33:28 +00:00
:queue <a collection of further interceptors>
:stack <a collection of interceptors already walked>}
```
`context` has `:coeffects` and `:effects` which, if this was a web
2016-08-11 06:09:43 +00:00
server, would be somewhat analogous to `request` and `response`
2016-08-11 04:33:28 +00:00
respectively.
2016-08-11 13:24:39 +00:00
`:coeffects` will contain the inputs required by the event handler
(sitting presumably on the end of the chain). So that's
data like the `:event` being processed, and the initial state of `db`. These are .
2016-08-11 04:33:28 +00:00
The handler-returned side effects are put into `:effects` including,
but not limited to, new values for `db`.
2016-08-11 06:09:43 +00:00
The first few interceptors in a chain (inserted by `reg-event-db`)
2016-08-11 04:33:28 +00:00
have `:before` functions which __prime__ the `:coeffects`
2016-08-11 06:09:43 +00:00
by adding in `:event`, and `:db`. Of course, other interceptors can
add further to `:coeffect`. Perhaps the event handler needs
2016-08-11 04:33:28 +00:00
data from localstore, or a random number, or a
DataScript connection. Interceptors can build up the coeffect, via their
`:before`.
Equally, some interceptors in the chain will have `:after` functions
2016-08-11 06:09:43 +00:00
which process the side effects accumulated into `:effects`
2016-08-11 04:33:28 +00:00
including but, not limited to, updates to app-db.
2016-08-13 06:34:25 +00:00
### Self Modifing
2016-08-11 04:33:28 +00:00
Through both stages (before and after), `context` contains a `:queue`
of interceptors yet to be processed, and a `:stack` of interceptors
2016-08-12 13:49:15 +00:00
already done.
2016-08-11 04:33:28 +00:00
2016-08-13 06:34:25 +00:00
In advanced cases, these values can be modified by the Interceptor
2016-08-11 04:33:28 +00:00
functions through which the `context` is threaded.
2016-08-11 06:09:43 +00:00
What I'm saying is that interceptors can be dynamically added
2016-08-13 06:34:25 +00:00
and removed from the `:queue` by existing Interceptors.
2016-08-11 04:33:28 +00:00
### Credit
2016-08-12 13:49:15 +00:00
> All truths are easy to understand once they are discovered <br>
> -- Galileo Galilei
2016-08-11 04:33:28 +00:00
2016-08-12 13:49:15 +00:00
This elegant and flexible arrangement was originally
designed by the talented
[Pedestal Team](https://github.com/pedestal/pedestal/blob/master/guides/documentation/service-interceptors.md). Thanks!
2016-08-11 04:33:28 +00:00
### Write An Interceptor
Dunno about you, but I'm easily offended by underscores.
If our components did this:
```clj
(dispatch [:delete-item 42])
```
We'd have to write this handler:
```clj
(def-event-db
:delete-item
(fn
[db [_ key-to-delete]] ;; <---- Arrgggghhh underscore
(dissoc db key-to-delete)))
```
Do you see it there? In the event destructuring!!! Almost mocking us with that
2016-08-11 06:09:43 +00:00
passive aggressive, understated thing it has going on!! Co-workers
2016-08-11 04:33:28 +00:00
have said I'm "being overly sensitive", perhaps even horizontalist, but
you can see it, right? Of course you can.
What a relief it would be to rid of it, but how? We'll write an interceptor: `trim-event`
2016-08-11 04:33:28 +00:00
Once we have written `trim-event`, our registration will change to look like this:
```clj
(def-event-db
:delete-item
[trim-event] ;; <--- interceptor added
(fn
[db [key-to-delete]] ;; <--- no leading underscore necessary
(dissoc db key-to-delete)))
```
`trim-event` will need to change the `:coeffects` map (within `context`). More specifically, it will be
2016-08-11 13:24:39 +00:00
changing the `:event` value within the `:coeffects`.
2016-08-11 04:33:28 +00:00
2016-08-11 13:24:39 +00:00
`:event` will start off as `[:delete-item 42]`, but will end up `[42]`. `trim-event` will remove that
2016-08-11 04:33:28 +00:00
leading `:delete-item` because, by the time the event is
being processed, we already know what id is has.
2016-08-11 06:09:43 +00:00
And, here it is:
2016-08-11 04:33:28 +00:00
```clj
(def trim-event
(re-frame.core/->interceptor
2016-08-11 13:24:39 +00:00
:id :trim-event
2016-08-11 04:33:28 +00:00
:before (fn [context]
(let [new-event (-> context
:coeffects
2016-08-11 06:09:43 +00:00
:event ;; extra event from coeffects
2016-08-11 04:33:28 +00:00
rest ;; remove first element
vec) ;; list->vector
(assoc-in context [:coeffects :event] new-event)))))
```
As you read this, look back to what a `context` looks like.
Notes:
2016-08-11 06:09:43 +00:00
1. We use `->interceptor` to create an interceptor (but it just a map)
2016-08-11 04:33:28 +00:00
2. Our interceptor only has a `:before` function
3. Our `:before` is given `context`. It modifies it and returns it.
4. There is no `:after` for this Interceptor. It has nothing to do
with the backwards processing flow of effects. It is concerned only
with coeffects in the forward flow.
2016-08-13 06:34:25 +00:00
####Wrapping Handlers
2016-08-11 04:33:28 +00:00
2016-08-11 06:09:43 +00:00
We're going well. Let's do an advanced wrapping.
2016-08-11 04:33:28 +00:00
2016-08-11 06:09:43 +00:00
How would you wrap a handler in an interceptor?
There's two kinds of handler:
2016-08-11 04:33:28 +00:00
- the `-db` variety registered by `reg-event-db`
- the `-fx` variety registered by `reg-event-fx`
Let's do a `-db` variety. This is what a `-db` handler looks like:
2016-08-11 06:09:43 +00:00
```clj
(fn [db event] ;; takes two params
(assoc db :flag true)) ;; returns a new db
```
And here is a function which turns a given handler into an interceptor:
2016-08-11 04:33:28 +00:00
```clj
(defn db-handler->interceptor
[db-handler-fn]
(->interceptor
:id :db-handler
:before (fn [context]
(let [{:keys [db event]} (:coeffects context) ;; extract db and event from coeffects
new-db (db-handler-fn db event)] ;; call the handler
(assoc-in context [:effects :db] new-db)))))) ;; put db back into :effects
```
2016-08-13 06:34:25 +00:00
## Summary
2016-08-11 04:33:28 +00:00
2016-08-11 06:09:43 +00:00
In this tutorial, we've learned:
__1.__ When you register an event handler, you can supply a collection of interceptors:
2016-08-11 06:09:43 +00:00
```
(reg-event-db
:some-id
[in1 in2] ;; <--- a chain of 2 interceptors
(fn [db v] ;; <-- real handler here
....)))
```
__2.__ When you registering an event handler, you are associating an event id with a chain of interceptors including:
- the ones your supply
- an extra one on the end, which wraps the handler itself
- a couple at the beginning of the chain, put there by the `reg-event-db` or `reg-event-fx`.
2016-08-11 06:09:43 +00:00
__3.__ Interceptors do interesting things:
- add to coeffects (data inputs to the handler)
- process side effects (returned by a handler)
2016-08-11 13:24:39 +00:00
- produce logs
2016-08-13 06:34:25 +00:00
- further process
2016-08-11 04:33:28 +00:00
In the next Tutorial, we'll look at (side) Effects in more depth. Later again, we'll look at Coeffects.
2016-08-11 04:33:28 +00:00
2016-08-13 06:34:25 +00:00
## Appendix
2016-08-11 04:33:28 +00:00
2016-08-13 06:34:25 +00:00
### The Builtin Interceptors
2016-08-11 04:33:28 +00:00
re-frame comes with some builtin Interceptors:
- __debug__: log each event as it is processed. Shows incremental [`clojure.data/diff`](https://clojuredocs.org/clojure.data/diff) reports.
- __trim-v__: a convenience. More readable handlers.
And some Interceptor factories (functions that return Interceptors):
- __enrich__: perform additional computations (validations?), after the handler has run. More derived data flowing.
- __after__: perform side effects, after a handler has run. Eg: use it to report if the data in `app-db` matches a schema.
- __path__: a convenience. Simplifies our handlers. Acts almost like `update-in`.
In addition, [a Library like re-frame-undo](https://github.com/Day8/re-frame-undo) provides an Interceptor
factory called `undoable` which checkpoints app state.
To use them, first require them:
```Clojure
(ns my.core
(:require
[re-frame.core :refer [debug path]])
```