re-frame/docs/WIP/MentalModelOmnibus.md

11 KiB

Mental Model Omnibus

The re-frame docs initially focus on the "domino cascade" narrative. And that works well for explaining the mechanics,
and, hopefully, gets you writing your first app as soon as possible.

But there's other interesting perspectives on re-frame which will considerably deepen your understanding of its design, and how to get the best from it.

This tutorial is a tour of these ideas, justifications and insights. It is a little rambling, but I believe you'll be glad you persisted. By the end, you may have had a couple of "Oh, now I get it" moments.

What Problem Does It Solve?

First, we decided to build our SPA apps with ClojureScript, then we choose [Reagent], then we had a problem. It was mid to late 2014.

For all its considerable brilliance, Reagent (+ React) delivers only the 'V' part of a traditional MVC framework.

But apps involve much more than V. We tend to build quite complicated apps. Where does the control logic go? How is state stored & manipulated? etc.

We read up on [Pedestal App], [Flux], [Hoplon], [Om], early [Elm], etc and re-frame is the architecture that emerged. Since then, we've kept a close eye on further developments like the Elm Architecture, Om.Next, BEST, Cycle.js, Redux, etc. They have taught us much although we have often made different choices.

re-frame does have M, V, and C parts but they aren't objects.
It is sufficiently different in nature from (traditional, Smalltalk) MVC that calling it MVC would be confusing. I'd love an alternative.

Perhaps it is a RAVES framework - Reactive-Atom Views Event Subscription framework (I love the smell of acronym in the morning).

Or, if we distill to pure essence, DDATWD - Derived Data All The Way Down.

TODO: get acronym down to 3 chars! Get an image of stacked Turtles for DDATWD insider's joke, conference T-Shirt.

Guiding Philosophy

First, above all we believe in the one true [Dan Holmsand], creator of Reagent, and his divine instrument the ratom. We genuflect towards Sweden once a day.

Second, we believe in ClojureScript, immutable data and the process of building a system out of pure functions.

Third, we believe in the primacy of data, for the reasons described in the main README. re-frame has a data oriented architecture. It implements an infinite loop of Derived data.

Fourth, we believe that Reactive Programming is one honking great idea. Wow did we ever live without it. It is a quite beautiful solution to one half of re-frame's data conveyance needs, but we're cautious about taking it too far - as far as say cycle.js. It doesn't take over everything in re-frame - it just does part of the job.

Finally, many years ago I programmed briefly in Eiffel where I learned about command-query separation. Each generation of programmers seems destined to rediscover this principle - CQRS is a recent re-rendering. And yet we still see read/write cursors and two way data binding being promoted as a good thing. Just say no. As programs get bigger, their use will encourage control logic into all the wrong places and you'll end up with a tire fire of an Architecture. IMHO.

It Does Physics

Remember this diagram from school? The water cycle. Two stages, involving water in different phases, being acted upon by different forces: gravity working one way, evaporation/convection the other.

logo

To understand re-frame, imagine data flowing instead of water. re-frame provides the "conveyance" of the data - the gravity, evaporation and convection. You design what's flowing and then you hang functions off the loop at various points to look after the data's phase changes.

Sure, right now, you're thinking "lazy sods - make a proper Computer Science-y diagram". But, no. Joe Armstrong says "don't break the laws of physics" - I'm sure you've seen the videos - and if he says to do something, you do it (unless Rich Hickey disagrees, and says to do something else). So, this diagram, apart from being a plausible analogy, and bordering on useful, is practically PROOF that re-frame is doing physics.

It does Event Sourcing

How did that exception happen, you wonder, shaking your head?
What did the user do immediately prior to the exception? What state was the app in that this event was so disastrous?

To debug, you need to know this information:

  1. the state of the app immediately before the exception
  2. What final event then caused your app to fall in a screaming mess.

Well, with re-frame you need to record (have available):

  1. A recent checkpoint of the app state in app-db (perhaps the initial state)
  2. all the events dispatched since the last checkpoint, up to the point where the exception occurred.

Note: that's all just data. Pure, lovely loggable data.

If you have that data, then you can reproduce the exception.

re-frame allows you to time travel, even in a production setting. Install the "checkpoint" state into app-db and then "play forward" through the collection dispatched events.

The only way the app "moves forwards" is via events. "Replaying events" moves you step by step towards the exception causing problem.

This is perfect for debugging assuming, of course, you are in a position to capture a checkpoint, and the events since then.

Here's Martin Fowler's description of Event Sourcing.

It does a reduce

Here's an interesting way of thinking about the re-frame data flow ...

First, imagine that all the events ever dispatched in a certain running app were stored in a collection. So, if when the app started, the user clicked on button X the first item in this collection would be the event generated by that button, and then, if next the user moved a slider, the associated event would be the next item in the collection, and so on and so on. We'd end up with a collection of event vectors.

Second, remind yourself that the combining function of a reduce takes two parameters:

  1. the current state of the reduction and
  2. the next collection member to fold in.

Then notice that reg-event-db event handlers take two parameters too:

  1. db - the current state of app-db
  2. v - the next event to fold in

Which is the same as a combining function in a reduce!!

So now we can introduce the new mental model: at any point in time, the value in app-db is the result of performing a reduce over the entire collection of events dispatched in the app up until that time. The combining function for this reduce is the set of event handlers.

It is almost like app-db is the temporary place where this imagined perpetual reduce stores its on-going reduction.

Now, in the general case, this perspective breaks down a bit, because using reg-event-fx:

  1. event handlers can produce effects beyond just application state changes.
  2. Event handlers sometimes need coeffects (arguments) in addition to db and v.

But, even if it isn't the full picture, it is a very useful and interesting mental model. We first saw it in Elm's early use of foldp (fold from the past), which was later enshrined in the Elm Architecture.

It does FSM

Any sufficiently complicated GUI contains an ad hoc, informally-specified, bug-ridden, slow implementation of a hierarchical FSM
-- my eleventh rule

event handlers collectively implement the "control" part of an application. Their logic interprets arriving events in the context of existing state, and they "step" the application state "forward".

events act a bit like the triggers in a finite state machine, and the event handlers act like the rules which govern how the state machine moves from one logical state to the next.

The "logical state" will be a function over the values in app-db. In the simplest case app-db will contain a single value which represents logical state.

Not every app has lots of logical states, but some do, and if you are implementing one of them, then formally recognising it and using a technique like State Charts will help greatly in getting a clean design and fewer bugs.

The beauty of re-frame from a FSM point of view is that all the state is in one place - unlike OO systems where the state is distributed (and synchronized) across many objects. So implementing your control logic as a FSM is both possible and natural in re-frame, whereas it is often difficult and contrived to do so in other kinds of architecture (in my experience).

So, members of the jury, I put it to you that:

Depending on your app, this may or may not be a useful mental model,
but one thing for sure ...

Events - that's the way we roll.

Data Oriented Design

In the readme ... XXX

Events are data - [:delete-item 42]

That's almost like a function call (delete-item 42). Kinda. So why prefer data?

Using data gives us:

  • late binding
  • logability and event sourcing
  • a more flexible version of "partial" (curring)

Derived Data

There's a video I'd like you to watch from StrangeLoop (40 mins, sorry).

XXX

If you have then, given the explanation above, you might twig to the idea that app-db is really a derived value (of the perpetual reduce).

And yet, it acts as the authoritative source of state in the app. And yet, it isn't, it is simply a piece of derived state. And yet, it is the source.

Hmm. This is an infinite loop of sorts. Derived data is flowing around the loop, reactively, through pure functions. There is a pause in the loop whenever we wait for a new event, but the moment we get it, it's another iteration of the "derived data" FRP loop.

Derived values, all the way down, forever.

Good news. If you've read this far, your insiders T-shirt will be arriving soon - it will feature turtles and xkcd. We're still working on the hilarious caption bit. Open a repo issue with a suggestion.

Prefer Dumb Views - Part 1

Many events are dispatched by the DOM in response to user actions.

For example, a button view might be like this:

(defn yes-button
  []
  [:div  {:class "button-class"
          :on-click  #(dispatch [:yes-button-clicked])}
          "Yes"])

Notice that on-click DOM handler:

  #(dispatch [:yes-button-clicked])

With re-frame, we want the DOM as passive as possible. We do not want our views containing any imperative control logic. All of that should be computed by event handlers.

We want that "on-click" as simple as we can make it.

Rule: views are as passive and minimal as possible when it comes to handling events. They dispatch pure data and nothing more.

Prefer Dumb Views - Part 2

Neither do we want views computing the data they render.
That's the job of a subscription:

So this is bad:

(defn show-items
  []
  (let [sorted-items (sort @(subscribe [:items]))]  ;; <--
    (into [:div] (for [i sorted-items] [item-view i]))))

The view is not simply taking the data supplied by the