Skip to content

introduction

Kenneth Tilton edited this page May 8, 2023 · 37 revisions

preface

This document will help the curious decide if they want to try programming Matrix-powered front ends.

We describe the Matrix reactive engine just enough to motivate the developer experience we find so productive, and share a bit of code from a demo project, rxTrak.

availability

Matrix began as Common Lisp Cells, and has now been ported to Clojure/ClojureScript, native JavaScript, and ClojureDart.

While Matrix offers a solution to complex state management in general, the big win comes when applied to GUI programming, where state management quickly scales to the unmanageable.

Programs are dominated by information processing, unless they have UIs, in which case there is this giant circle of information processing that makes the logic look like a dot. But I am not going to go there. I do not do that part.

— Rich Hickey
Clojure/Conj 2017

available GUI integrations

Matrix has been integrated with several front-end frameworks:

  • Common Lisp: qooxdoo.js, Tcl/Tk, GTk, and OpenGL;

  • JavaScript: straight HTML/CSS;

  • ClojureScript HTML/CSS, ReactNative, and React for the web; and

  • ClojureDart-based mxFlutter is our current effort.

history

We stumbled onto Cells — re-inventing prior art — in the mid 1990s. The origin story will be a worthy read for those who do not enjoy reading unmotivated magic. More write-ups and links to live demos can be found in the bibliography.

hello, Matrix

Now let us look at the core Matrix reactive engine, and how it works with the CLJS+HTML/CSS stack.

Matrix GUI mechanisms will be familiar to GUI-savvy readers. Matrix just arranges the same things differently.

tree, objects, and properties <<->> matrix, models, and cells

A Matrix app consists of a tree of objects whose properties are made reactive by structures we call cells. We call the objects models, in the sense of "working models" that react to change around them. We call the tree a matrix, for reasons explained below.

Developers use the Matrix API to generate Cells for any property they wish to be "reactive", meaning they can:

  • be computed from other properties, and kept up to date automatically;

  • used by other properties for their computation;

  • propagated as needed outside the emergent computed dataflow, such as when a fresh value for an HTML attribute is propagated to the actual DOM;

  • accept values from outside the Matrix universe, such as user mouse clicks or XHR responses; or

  • some combination of the above.

As soon as Cells are generated:

  • Matrix instance initialization "hides" them behind a getter/setter API; and

  • manages state change automatically.

Here is a snippet example from the TodoMVC classic, where the spec says not to show the "beef" of the to-dos list until at least one to-do has been entered:

(section {:class  "main"
          :hidden (cF (mget (mx-todos me) :empty?))}
   ...etc...
Without subscribing or publishing, the hidden HTML attribute will track whether the mx-todos of the app are empty.

In general, Matrix models serve as proxies for any application state we wish to work reactively, such as DOM elements, localStorage, or XHRs.

In Depth: Implementations in different host languages

We want to stay out of the implementation weeds, but to ground things a bit, the model objects are implemented as:

  • maps in CLJ/S, with a type property that is a subtype of :tiltontec.cell.base/model;

  • CLOS instances in Common Lisp; or

  • class instances in JavaScript.

The properties are:

  • CLJ/S map key-value pairs;

  • CLOS slots; or

  • JS class members.

As mentioned, they get their reactive powers from structures called cells. As a shorthand, we refer to such properties themselves as cells.

cell types

Cells can get quite sophisticated in mediating reactive flow, but for our introductory purposes we just need to know that there are two fundamental kinds of cells:

  • ruled, or formulaic, cells which have a function that computes the value from other cells. The instance itself is available to the function as me, akin to this or self; and

  • input cells, which can be assigned to by conventional imperative code, usually event handlers, be they mouse clicks or XHR responses.

In a more advanced write-up, we will talk about ephemeral cells for events, lazy cells, and even synapses, anonymous cells that live within a ruled cell formula.

observers: first-class side effects

Great. Now we have a declarative paradigm in which changes to one cell trigger a chain reaction of changes to other cells. And we have a problem. Suppose in our example above, the hidden formula of the SECTION proxy comes up with a new value. How do we tell the browser to change the hidden attribute of the actual SECTION element?

So-called observers can be defined for a property, such as the hidden attribute for a DIV, and cover all instances of that type, or they can be defined ad hoc when we create an ad hoc rule for a property.

Tip

Many reactive systems refer to derived properties as "observers". Perhaps because of the observer pattern. The dictionary disagrees.

ob·​serv·​er | \ əb-ˈzər-vər \

Definition of observer : one that observes: such as a representative sent to observe but not participate officially in an activity (such as a meeting or war)

— Merriam-Webster.com

Derived properties certainly do participate in the larger data flow. In Matrixese, then we use observer for actions we need to kick off outside the Matrix data flow, such as directing the browser to update the DOM.

An exception that proves the rule: when coding complex UIs, we commonly want to help the user by automatically kicking off an application change that they might initiate themselves. Matrix lets observers sudo into participant mode to initiate such changes, but they must do so explicitly and their changes are deferred until the current change has been fully processed.

extended example

Here is a heavily-annotated example that may help ground this abstract description of how Matrix apps work. It shows the implementation of the footer from the TodoMVC classic:

(defn dashboard-footer []
  (footer {:class  "footer"
           :hidden (cF
                     ;; we navigate to the app-global 'rxs' list,
                     ;; then read its :empty? property, transparently establishing a dependency.
                     (mget (mx-rxs me) :empty?))}

    (span {:class   "todo-count"
           :content (cF
                      ;; 'mx-rx-items' likewise reads from app-global 'rxs' list,
                      ;; then reads the actual :items property. The moral: Matrix "sees"
                      ;; dependencies formed inside the call stack.
                      (pp/cl-format nil "<strong>~a</strong>  item~:P remaining"
                        ;; 'rx-completed' shows how we can hide `(mget rx :completed)`,
                        ;; again because dependencies are "seen" inside function calls.
                        ;; So here we end up with one dependency on the list itself, and
                        ;; dependencies on the :completed property of each Rx.
                        (count (remove rx-completed (mx-rx-items me)))))})

    (ul {:class "filters"}
      (for [[label route] [["All", "#/"]
                           ["Active", "#/active"]
                           ["Completed", "#/completed"]]]
        (li {} (a {:href     route
                   :selector label
                   :class    (cF
                               ;; Note that an individual HTML attribute can have 
                               ;; its own formula. This kind of detailed semantic
                               ;; relationship is lost with view functions having
                               ;; piles of subscriptions to a Flux-pattern store.
                               ;;
                               ;; 'mx-route' tracks another property of the app
                               ;; as a whole, the selected :route
                               (when (= (:selector @me) (mx-route me))
                                   "selected"))}
                 label))))

    (button {:class   "clear-completed"
             :hidden  (cF
                        ;; another DOM attribute formula. This fine granularity means
                        ;; minimal workload to propagate a state change fully, as 
                        ;; well as making app behavior trivial to follow.
                        (empty? (mget (mx-rxs me) :items-completed)))
             :onclick #(doseq [td (filter rx-completed (mx-rx-items))]
                         ;; this loop sets the :deleted property of all completed items.
                         ;; rx-delete! is just a function created to hide the generic setter 'mset!'
                         (rx-delete! td))}
      "Clear completed")))

property-oriented data flow

We mentioned how properties get their reactive power. We did not mention models, for good reason: all data flow is between properties. As just described, a "ruled" property can be defined as a function of other properties. When one of those properties changes, the computed property will be re-computed. Without glitches, if you know your reactive art.

Models simply provide the infrastructure to support data flow, and an aggregation of things to be animated by that data flow. More on that next.

Matrix, the tree and the name

We said a Matrix app was a tree of models. Each model has two properties:

  • parent, not a cell-mediated property. Not mutable during the life of a model. For life-cycle reasons, must be provided when instantiating a model; required for all but the root of a Matrix tree; and

  • kids, the optional children of a model, and optionally a ruled cell.

Our tree, then, is just inter-connected parents and children.

its alive!

The fact that kids is ruled means the matrix tree can change shape dynamically in response to events; the model population grows and shrinks. The largest matrix application starts out as a single model instance, then grows from that seed as the first kids rule gets evaluated and spawns descendants that recursively expand into the initial state of the app, perhaps a full-blown landing page.

Hence the name "matrix".

ma·​trix | \ ˈmā-triks \: something in which something else develops or forms

— Merriam-Webster.com

Glitches

Serious reactive engines must avoid so-called "glitches".

A glitch is a short-lived fault in a system, such as a transient fault that corrects itself
— https://en.wikipedia.org/wiki/Glitch[WikiPedia]

The problem arises when two paths in the dependency DAG lead from one node to another derived node; with the right combination of intervening dependencies, the derived node can compute twice, once with an out of date value because it is on the second path that will be refreshed.

This is indeed transient, but things can go seriously wrong before the state gets corrected. Not always, though. Matrix itself delivered complex enterprise software while still vulnerable to glitches, but eventually they broke an application and had to be resolved.

In depth: The Matrix Data Integrity contract

Matrix puts a positive spin on the issue of glitches by offering what we call a "Data Integrity" contract.

When application code assigns to some input cell X, the Cells engine guarantees:

  • recomputation exactly once of all and only state affected by the change to X, directly or indirectly through some intermediate datapoint. note that if A depends on B, and B depends on X, when B gets recalculated it may come up with the same value as before. In this case A is not considered to have been affected by the change to X and will not be recomputed.

  • recomputations, when they read other datapoints, must see only values current with the new value of X. Example: if A depends on B and X, and B depends on X, when X changes and A reads B and X to compute a new value, B must return a value recomputed from the new value of X.

  • similarly, client observer callbacks must see only values current with the new value of X; a corollary: should a client observer SETF a datapoint Y, all the above must happen with values current with not just X, but also with the value of Y prior to the change to Y; and finally

  • deferred “client” code must see only values current with X and not any values current with some subsequent change to Y queued by an observer

The D/X

After program correctness and efficiency, what matters is programmer productivity, a function of the expressive power of our programming mechanisms. Matrix has been designed from the beginning to make it easy for developers to deliver functionality. Here are the key wins.

co-location

co-location seems to be the silly jargon for not scattering relevant program logic to the four winds in worship of the false god of "separation of concerns". [Discuss](https://news.ycombinator.com/item?id=30166318)

With Matrix, when working on X, we have everything related to X in front of us. With HTML, we can even have the CSS right in front of us, and with individual style properties with their own formulas. Of course the class attribute is supported, but both CLJS and JS have more expressive power.

Aside from CSS, the developer need not wrestle with Flux to create responsive U/X, they just need to know how the U/X they are coding should track specific other app state and code up the derivation. The uni-directional Grail of Flux external stores is automagically detected by Matrix internals. And now all the semantics of any given component will be right in front of us as we work on that part of the U/X.

omniscience and omnipotence: global reach

Because a model knows its parent as well as its children, and because a rule has the model lexically available in me, rule code can navigate from me to any other object to get a value for its computation.

Likewise, an event handler can navigate anywhere in the application to mutate any input cell. Gasp.

scoped global reach

In twenty-five years of practice, on very hard problems, this global reach has presented no problems. Consider that navigation is not a Hail Mary jump to a global reference. The "reach" navigates outwards from me. We can write our own traversal, or use a Matrix utility that has a bunch of options to constrain this search, which defaults to checking:

  • me;

  • my children, depth first, left-to-right;

  • recursively checking my parent, which skips me when checking its children, my siblings; and

  • stopping when it finds the model whose value I want, which model is specified by a test I provide.

The consequence for the D/X is that the developer who needs a value to compute a derived value simply has to know where to find it—​by name, type, or arbitrary test—​given the inside-out search algorithm.

Note
The alternative to global reach is Flux, which fails the "expressive" test in several ways. More below.

instance-oriented programming

At the time we instantiate models, different instances of the same type can be provisioned with different cells for the same property. An example will make that clearer: two DIV proxies can have two different rules for deciding the class of the DIV. Obviously handy.

Even better, we can provide a new instance with an ad hoc property. So our two DIVs can have additional properties to get their job done. We will see an example of this in rxTrak where we have a DIV dispatch an XHR to look up adverse events in an NIH database and capture the results in a custom property.

the Grail: object re-use

The astute reader will notice that the Grail of OO re-use has been found, albeit by cheating: a Matrix type is reusable because, while the expression of a DIV class is still fixed, its derivation now can vary to suit a given use. Object types simply do not dictate as much as they once did or, put another way, are just more configurable.

A good example will be seen below when we get to our demo project, and endow a DIV with a system clock driven timer.

omnipresence: universal reactivity

One difference with Matrix is that all long-lived application state consists of models and cells. View elements, domain objects, XHRs, localStorage, the system clock, and anything else we want to be reactive can be wrapped in proxy models. And because the reactive paradigm works so well, we want as much as possible to be reactive.

Reactivity is a contagious paradigm, and Matrix encourages its spread.

rxTrak

[WIP]

Our demo project for Matrix will be rxTrak, the TodoMVC classic converted to a personal medication tracking application.

We did rxTrak so we could include two async features:

By the way, our mxXHR add-on to Matrix is a good example of how to make reactive something that is not reactive, XHRs. This so-called "wrapping" process is straightforward once one is comfortable with Matrix.

We will be extending this section with code examples RSN.

In the meantime, here is another write-up involving the JS version, with live, working CodePen examples you can play with yourself.

[To be continued]

contrasts

None of this will make much sense until one has a chance to use Matrix in anger, but it may help to point out contrasts with today’s conventional reactive front-end frameworks, with which we have had a few years experience.

"in place" state management. Flux need not apply.

With Matrix, no separate Flux-pattern store is needed to achieve uni-directional, coherent state flow. The application itself is the reactive DAG, automatically detected at run time.

Matrix manages state in place.

We avoid the burdens of:

  • architecting the store;

  • composing actions/reducers; and

  • mapping the application structure to the schema of the external store, now that the latter has been abstracted from the former.

In depth: A deeper look at the problem with Flux

Flux got invented because Facebook engineers could not figure out how to maintain a chat counter. Their very plausible solution was to:

  • Balkanize GUI application state out into an explicitly designed in-app database, the "store", with a schema abstracted from the noisy shape of the GUI application;

  • create an API, if you will, of allowed mutating "actions" or "events" to modify the store;

  • then let the noisy GUI structure work out how to live with the store, both:

    • discerning how to subscribe to specific elements in the store; and

    • work out how to use the mutating API to express user gestures.

Plausible, but exactly wrong.

Make everything as simple as possible, but not simpler.
— Albert Einstein
ascribed

===== simpler considered harmful

Consider this. The developer is working on a GUI application. These are notoriously hard because of the many interdependencies of the presentation. GUI applications tend to have multiple ways for a user to accomplish the same thing. These apps also need to reflect any given state change in multiple ways. The very content presented by a dynamic GUI application must change over time.

The intimate blending of state with GUI components is a feature, not a bug.

Isolating state once owned by GUI components, and concocting a new "ideal" schema independent of the GUI application, just makes it harder to do what developers still have to do: make the GUI application work consistently as a whole. With a separate store, a developer working on a UI component now must work out how to get the state they need from that store. When they want a component event handler to alter the state seen by other UI components, they have to work out what abstract action/event will leave the store with the state where the other components will find it. They may even have to architect new store schema and new reducers when extending functionality.

Understandably frustrated by their chat counter, Facebook engineers made state management simpler than was possible, and Redux was the necessarily bloated outcome.

no explicit publish or subscribe

With Matrix, we do not publish or subscribe explicitly. Matrix automatically and transparently:

  • detects when formulas use other cells;

  • records those usages to form the implicit DAG; and

  • automatically propagates change through that DAG when application code, usually handlers, write to input cells.

Handlers do not dispatch actions/events that lead to complex state mutations coded elsewhere, they just change one cell of one model.

no special "view" functions

When it comes to rendering, we just write code that builds a proxy DOM, and observers transparently and efficiently maintain the actual DOM. There is no VDOM built or diffed to come up with DOM changes to be applied. e.g., When a proxy DIV decides on a new class for a DIV, an observer uses the DOM API to change that attribute.

Instead of wrestling with view lifecycle methods and artificially arranging for view functions to re-render, with Matrix we just write code to build a view and Matrix internals keep the DOM updated, efficiently and minimally.

Where Matrix wraps React, Matrix uses setState to trigger re-rendering only when necessary, and it automatically knows the minimum DOM that needs re-rendering.

bibliography

Here are other Matrix write-ups and on-line working code using the JS version of Matrix, with the better write-ups first:

Obligatory TodoMVC, in CLJS

This is both a high quality write-up and a working TodoMVC implementation.

Scroll down to the end of the read-me to find a link to part II of the write-up, which then leads to part III, a crazy deep dive into the detailed flow of a state change. It reminds us of how many things we need to get right when changes happen—​and TodoMVC is rather primitive when it comes to interdependency.

This includes the live carousel grace a embedded CodeSandbox. The idea is to do great things with reactive programming without introducing a heavy mechanism such as Flux. The quotation carousel of "simplicity" quotations is crazy slick but uses just HTML.

No joke, this is exactly how I stumbled onto re-inventing Matrix. It happens also to be an ideal intro into how Matrix works, because you can see how we got from "pull" to "push", hence reactive data flow.

If, like me, reading about some magical library drives you nuts until you have some grasp of how it works, this is ten-minute read that will fill in that gap.

The Cells Manifesto classic

Surprisingly complete and comprehensive, given that I knocked it off in one sitting in about two hours, when I just meant to do a quick overview. Strongly recommended for the bit on "Data Integrity".

CodePen JS Matrix Intro

This is an application within an application, if you can beleive that. The CodePen code presents the evolution of maybe half of TodoMVC, which is also a working example. I scare myself sometimes.

Working applications

OK, these are not write-ups, they are just showcase apps developed with Matrix:

  • A search tool for Hacker News’s monthly AskHN question, Who’s Hiring browser. This is the JS version, using HTML/CSS mxWeb as the front-end platform.

  • Tilton’s Algebra, built with Allegro Common Lisp Cells driving the qooxdoo JS library.