Skip to content

Historical

What is the problem?

First, we decided to build our SPA apps with ClojureScript, then we chose Reagent, then we had a problem. It was mid-2014.

For all its considerable brilliance, Reagent (+ React) delivers only the 'V' part of a traditional MVC framework. (Or, at least the React of that time did. Since then they have turned it into more of a Frankenstein)

But apps involve much more than V. We build quite complicated SPAs which can run to 40K lines of code. So, I wanted to know: where does the control logic go? How is state stored & manipulated? etc.

We read up on Pedestal App, Flux, Hoplon, Om, early Elm and re-frame is the architecture that emerged. Since then, we have tried to keep an eye on further developments like the Elm Architecture, Om.Next, BEST, Cycle.js, Redux, etc. And that has taught us to appreciate what we have.

re-frame does have parts which correspond to M, V, and C, 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).

Perhaps 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 introductions. re-frame has a data-oriented, functional architecture.

Fourth, we believe that Reactive Programming is one honking good idea. How 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, a long time ago in a galaxy far far away, I was lucky enough to program in Eiffel where I was exposed to the idea of command-query separation. The modern rendering of this idea is CQRS (see resources here). But, even today, we still see read/write cursors and two-way data binding being promoted as a good thing. Please, just say no. We already know where that goes. As your programs get bigger, the use of these two-way constructs will encourage control logic into all the wrong places and you'll end up with a tire-fire of an Architecture.
Sincerely, The Self-appointed President of the Cursor Skeptic's Society.

It does Event Sourcing

How did that error happen, you puzzle, shaking your head ruefully? What did the user do immediately prior? What state was the app in that this event was so problematic?

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 error

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

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

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

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

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

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

This is perfect for debugging assuming, of course, you are in a position to capture a checkpoint of app-db, 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 (yes, event sourcing again). 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 arguments:

  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 arguments also:

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

Interesting. That's 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 of reg-event-fx (has -fx on the end, not -db) which allows:

  1. Event handlers to produce effects beyond just application state changes.
  2. Event handlers to have 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 were first exposed to this idea via Elm's early use of foldp (fold from the past), which was later enshrined in the Elm Architecture.

Derived Data All The Way Down

For the love of all that is good, please watch this terrific StrangeLoop presentation (40 mins). See what happens when you re-imagine a database as a stream!! Look at all the problems that evaporate. Think about that: shared mutable state (the root of all evil), re-imagined as a stream!! Blew my socks off.

If, by chance, you ever watched that video (you should!), you might then twig to the idea that app-db is really a derived value ... the video talks a lot about derived values. So, yes, app-db is 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. Etc.

This is an infinite loop of sorts - an infinite loop of derived data.

It does FSM

Any sufficiently complicated GUI contains an ad hoc, informally-specified, bug-ridden, slow implementation of a hierarchical Finite State Machine
-- me, trying too hard to impress my two twitter followers

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

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.

In the simplest case, app-db will contain a single value which represents the current "logical state". For example, there might be a single :phase key which can have values like :loading, :not-authenticated :waiting, etc. Or, the "logical state" could be a function of many values in app-db.

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 synchronised) across many objects. So implementing your control logic as a FSM is fairly natural in re-frame, whereas it is often difficult and contrived 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 is for sure ...

Events - that's the way we roll.