Management of Derived State

My previous article Introduction to Composable Nanostates introduced a problem inherent to UI design and development. This article covers some approaches to the problem.

The Problem (Again)

The problem of state complexity was covered in the previous article. Here is a quick summary of the problem:

  • In many UI applications many values (colors, styles, displayed messages) depends on a set of “primitive” values (some values in model, basic inputs)
  • Dependency graphs may grow large and complex. There may be multiple layers of “computed” values based on other ones
  • Many UI bugs come from the manual state management: base (or input) values are updated and derived values are not
  • One of the possible solutions is to have a “single source of truth” (only basic mutable values) and update all the derived values automatically. Developers are not allowed to mutate explicit values by hand.

Solution to the problem eliminates a bug nest, simplifies debugging and helps developer to find edge cases in requirements.

The following chapters cover some popular approaches present in the frameworks and libraries and analyses their strengths and weaknesses.

Plain Observables and MVC

This is probably a historically first approach targeting the problem. The idea behind it is very simple:

  1. An object (observable) exposes some state.
  2. An object defines a way to register a listener (observer) to be notified when an internal value changes
  3. An object notifies all observers (fires an event) when its internal state change.

An actual change event could come in different flavors. It could be a simple “I changed” notification. Or it may provide more details like “Elements from 5 to 10 where removed”.

There are clear benefits of this approach:

  • An observable does not care about its consumers.
  • When used correctly, it solves the “derived state problem”. All the listeners could update its internal state.

However, this approach have some disadvantages:

  • Listener management is very verbose. Adding/removing listeners requires much code. Actual code logic is hidden behind subscription one.
  • This approach is prone to memory leaks and performance issues caused by a listeners not unregistered from the underlying observable.
  • There could be a “diamond dependency” problem. Values B and C depends on A. Value D depends on both B and C. How many times D will be updated when A changed? In most libraries the answer is 2. On more complex graphs there may be more unnecessary updates. This may be a performance issues. In my practice I had a texture (bitmap) dependent on several inputs and complex data graph. Excessive updates where visible to a user so this is not just a fictional problem but a real one.
  • Coarse-grained observables. This is a common problem with many implementations (not the approach itself). They use a large object as an “observable” containing several (or even many) simple properties. Only the object is observable, not the properties. So you have to filter events if you are interested in only some properties.
  • Lack of abstraction is a direct consequence of the previous problem. Observer depends on the whole observable class even if it requires only some of its properties.
  • If an observer is also observable, developers are required to fire events by hand.

Java Swing is an example of this approach.

Property Binding

The next attempt to solve this problem was an introduction of property binding frameworks. In these frameworks you always operate on two objects and copy some property of an object to another object each time the source property changes. The binding could be bi-directional or unidirectional.

This approach solves a problem of coarse-grained observables and corresponding lack of abstraction. Your object provides some “input” properties. You do not care what is a source of values (who sets it). You just care about having them in place.

However, many of the “Plain Observable” problems are still not solved. New problems are introduced:

  • What if in two-way binding properties “could not” agree on its value? Each observable “mutates” its internal property.
  • Usually there is no clear distinction between “input” and “output” properties. They implement exactly the same interface. What if I try to change an “output” property of an object?
  • Developers are forced to create tons of classes just for binding. You need a whole class with bunch of inputs and outputs even for the simplest operations like addition and subtraction.

To some degree this approach is used in

  • AngularJS — there is a two-way binding between scopes (isolated and outside). This particular library complicates the problem by not having explicit observables. It uses heuristics instead of explicit observable events and resynchronizes all scopes (observables) pretty often (for example, on each mouse move or key press). This could cause huge performance problems. It tries to solve the problem with the listener management. However, it is not clear if the cure is better than the disease. However, it have no way to manage something not on scope. You can not write some “abstract non-component graph”. For example, you could not leverage angular binding inside your networking library (angular requires a UI component).
  • JavaFX — it “binds property of objects”. It is a “multi-paradigm” library, so other approaches are used as well.

Recalculate Everything

This approach targets a core of the problem — synchronization of cached state. It says “do not store anything, just recalculate everything when something changes”.

It sounds like very good and functional way of doing things. Definitely, it have the following advantages:

  • No listener management at all. You could not forget to update a listener. You could not forget to unsubscribe. There is no concept of listener at all.
  • Better abstractions. You could pass only required values into the “observer” (factory of the derived value or state).
  • Easily testable. In most of the code you use pure functions. It is very easy to write unit-tests for them.

However, they do not solve some other problems and introduce few others:

  • Performance problem caused by a huge amount of data to be calculated. Non-changed data could be recalculated as well. Consider an example with the texture. It will be updated on each mouse movement. What a waste of resources!
  • Many frameworks tends to have only one “data tree” (or only a very few ones). So the problem of state management is changed to a problem of “tree decomposition and composition”. Each small “service” have to find a place in the tree and then rebuild it completely. Maybe not a big price for the advantages, but still not fun to manage/route events.
  • It requires a heavyweight UI “rendering” framework. They need a virtual DOM (or its analog) to be able to calculate the target representation efficiently. These libraries uses heuristics to determine required changes on physical primitives (DOM elements, swing components, etc…)
  • Mechanism used to represent component’s internal state is different from the mechanism used to represent “global” state.

Examples of this approach:

  • Flux architectures and ReactJS (as an implementation of a virtual DOM)
  • AngularJS could use this approach inside services (“between scopes”). There is no good way to cache intermediate values, but at least you will use “pure functions”.

“Monadic” First Class State

This approach admits an existence of “intermediate” state (it is essentially a cached state). It is based on old observables. However, a convenient API and abstractions are provided to manage “subscription management”. All the listener management is done by the library and developers could focus on the business logic. Examples of this functional dependency are:

//Knockout.js style
var derived = ko.computed(function() { return a() + b(); });
//Nanostates style
var derived = R(function(a, b) { return a + b; }, a, b);

You got most of the benefits of different approaches:

  • No manual listener or subscription management. They are managed by library.
  • Diamond dependency could be solved (nanostates library solves it).
  • Updates are precise, no unneeded recalculations.
  • Due to precise updates, there is no need to have a virtual DOM. UI library could be very thin.
  • Observables are very fine-grained. This allows better abstraction for “intermediate” values (functions could depend on individual values and not on complex objects).
  • Most of the functions are easily testable. You need “observable-aware” function if you create some “data graphs” inside it. But for pure calculations you could write a testable pure function.
  • With a proper API, the same approach could be used across multiple platforms. For example, I have an implementation of nanostates for Javascript (HTML bindings) and Scala implementation used with Vaadin (server-side framework), Swing and JavaFX. It is exactly the same state propagation library, the only difference is UI binding.

The very few disadvantages are:

  • It may require a paradigm shift in your head to get used to applicative/monadic coding practices (however, this style have a wider range of benefits. For example, Asynchronous operations could be written in the exactly same style because Promise/Future is a monad).
  • There is some “framework pollution” in the functions creating states (i.e. creating some intermediate observables). However, functions calculating values could be pure.

Examples of this approach:

  • JavaFX — there is a “first-class” property object. You could combine them in a functional way (by applying pure functions and combinators). However, Java language lack expressive power so the API is pretty cumbersome.
  • KnockoutJS
  • Flapjax-lang
  • My Nanostates/FRP library

Summary

I suggest looking on one of these “Monadic First Class State” libraries. They have many advantages and very few disadvantages compared to all others. I personally would suggest using my one. It could also operate on dynamic (based on values of observables) dependency graphs and does it pretty well. It also pretty platform/framework independent.

References: