re-frame/docs/WIP/Flow.md

9.3 KiB
Raw Blame History

Table Of Contents

Flow

It is time to talk of the reactive flow through dominoes 4-5-6. through dominoes It is time to talk about de-duplicated signal graphs.

This tutorial focuses mainly on how data flows between dominoes 3-4-5-6. We'll look at the underlying reactive mechanism.

BUT we'll start by looking at the overall picture ...

Interconnections

Ask a Systems Theorist, and they'll tell you that a system has parts and interconnections.

Human brains tend to focus first on the parts, and then, later, maybe on interconnections. But we know better, right? We know interconnections are often critical to a system. "Focus on the lines between the boxes" we lecture anyone kind enough to listen (in my case, glassy-eyed family members).

In the case of re-frame, dominoes are the parts, so, tick, yes, we have looked at them first. Our brains are happy. But what about the interconnections?

If the parts are functions, what does it even mean to talk about interconnections between functions? To answer that question, I'll rephrase it as: how are the domino functions composed?

At the language level, Uncle Alonzo and Uncle John tell us how a function like count composes:

(str (count (filter odd?  [1 2 3 4 5])))

We know when count is called, and with what argument, and how the value it computes becomes the arg for a further function. We know how data "flows" into and out of the functions.

Sometimes, we'd rewrite this code as:

(->>  [1 2 3 4 5]
      (filter odd?)
      count
      str)

With this arrangement, we talk of "threading" data through functions. It seems to help our comprehension to frame function composition in terms of data flow.

re-frame delivers architecture by supplying the interconnections - it threads the data - it composes the dominoes - it is the lines between the boxes.

But re-frame has no universal method for this. The technique it uses varies from one domino neighbour pair to the next.

Between 1 and 2

There's a queue.

When you dispatch an event, it is put into a FIFO queue to be processed "vey soon".

It is important to the design of re-frame that event processing is async.

On the end of the queue, is a router which (very soon) will:

  • pick up events one after the other
  • for each, it extracts kind of event (first element of the event vector)
  • for each, it looks up the associated event handler and calls it

Between 2 and 3

I lied above.

I said the router called the event handler associated with an event. This is a useful simplification, but we'll see in future tutorials that there's more going on.

I'll wave my hands about now and give you a sense of the real story.

Instead of there being a single handler function, there's actually a pipeline of functions which we call an interceptor chain. The handler you write is inserted into the middle of this pipeline.

This function pipeline manages three things:

  • it prepares the coeffect for the event handler (the set of inputs required by the handler)
  • it calls the event handler (Domino 2)
  • it handles the effects produced by the event handler (Domino 3)

The router actually looks up the associated "interceptor chain", which happens to have the handler wrapped on the end.

And then it processes the interceptor chain. Which is to say it calls a

There's

  • calls the handler , looks at their first , looks at their first element, and runs the associated

Between 1 and 2 it is a queue & router, between 2 and 3 it is an interceptor pipeline, and along the 3-4-5-6 domino axis there's a reactive signal graph. The right tool for the job in each case, I'd argue.

While interconnections are critical to how re-frame works, you can happily use re-frame for a long time and be mostly ignorant of their details.

Which is a good thing - back we go to happy brains focusing on the parts.

1 -> 2

dispatch queues events and they are not immediately processed. Event handling is done async.

A router reads events from this queue, looks up the associated handler and calls it.

2 -> 3

Except I lied in the previous section. The router doesn't really look up a single "handler". Instead it looks up an interceptor chain. The method by which an Interceptor chain is executed is discussed in great detail shortly.

3->4->5->6

So now we are at the meat and potatoes. The real subject of this tutorial.

On Flow

Arguments from authority ...

Everything flows, nothing stands still. (Panta rhei)

No man ever steps in the same river twice for it's not the same river and he's not the same man.

Heraclitus 500 BC. Who, being Greek, had never seen a frozen river. alt version.

Think of an experience from your childhood. Something you remember clearly, something you can see, feel, maybe even smell, as if you were really there. After all you really were there at the time, werent you? How else could you remember it? But here is the bombshell: you werent there. Not a single atom that is in your body today was there when that event took place .... Matter flows from place to place and momentarily comes together to be you. Whatever you are, therefore, you are not the stuff of which you are made. If that does not make the hair stand up on the back of your neck, read it again until it does, because it is important.

Steve Grand

How Flow Happens In Reagent

To implement a reactive flow, Reagent provides a ratom and a reaction. re-frame uses both of these building blocks, so let's now make sure we understand them.

ratoms behave just like normal ClojureScript atoms. You can swap! and reset! them, watch them, etc.

From a ClojureScript perspective, the purpose of an atom is to hold mutable data. From a re-frame perspective, we'll tweak that paradigm slightly and view a ratom as having a value that changes over time. Seems like a subtle distinction, I know, but because of it, re-frame sees a ratom as a Signal. Pause and read this.

The 2nd building block, reaction, acts a bit like a function. It's a macro which wraps some computation (a block of code) and returns a ratom holding the result of that computation.

The magic thing about a reaction is that the computation it wraps will be automatically re-run whenever 'its inputs' change, producing a new output (return) value.

Eh, how?

Well, the computation is just a block of code, and if that code dereferences one or more ratoms, it will be automatically re-run (recomputing a new return value) whenever any of these dereferenced ratoms change.

To put that yet another way, a reaction detects a computation's input Signals (aka input ratoms) and it will watch them, and when, later, it detects a change in one of them, it will re-run that computation, and it will reset! the new result of that computation into the ratom originally returned.

So, the ratom returned by a reaction is itself a Signal. Its value will change over time when the computation is re-run.

So, via the interplay between ratoms and reactions, values 'flow' into computations and out again, and then into further computations, etc. "Values" flow (propagate) through the Signal graph.

But this Signal graph must be without cycles, because cycles cause mayhem! re-frame achieves a unidirectional flow.

Right, so that was a lot of words. Some code to clarify:

(ns example1
 (:require-macros [reagent.ratom :refer [reaction]])  ;; reaction is a macro
 (:require        [reagent.core  :as    reagent]))

(def app-db  (reagent/atom {:a 1}))           ;; our root ratom  (signal)

(def ratom2  (reaction {:b (:a @app-db)}))    ;; reaction wraps a computation, returns a signal
(def ratom3  (reaction (condp = (:b @ratom2)  ;; reaction wraps another computation
                             0 "World"
                             1 "Hello")))

;; Notice that both computations above involve de-referencing a ratom:
;;   - app-db in one case
;;   - ratom2 in the other
;; Notice that both reactions above return a ratom.
;; Those returned ratoms hold the (time varying) value of the computations.

(println @ratom2)    ;; ==>  {:b 1}       ;; a computed result, involving @app-db
(println @ratom3)    ;; ==> "Hello"       ;; a computed result, involving @ratom2

(reset!  app-db  {:a 0})       ;; this change to app-db, triggers re-computation
                               ;; of ratom2
                               ;; which, in turn, causes a re-computation of ratom3

(println @ratom2)    ;; ==>  {:b 0}    ;; ratom2 is result of {:b (:a @app-db)}
(println @ratom3)    ;; ==> "World"    ;; ratom3 is automatically updated too.

So, in FRP-ish terms, a reaction will produce a "stream" of values over time (it is a Signal), accessible via the ratom it returns.

Okay, that was all important background information for what is to follow.