Reactivity and Caching

Functionally reactive programming techniques have seen increasing usage lately, as a means of simplifying code, particularly in situations where views can react to model data. Reactive programming can replace a lot of error-prone imperative code dedicated to responding to data changes, allowing synchronization of views and data to be written more precisely, without redundancy of intent.

As I have been working on reactive programming in xstyle, I believe that reactivity is actually a natural extension of what I like to call resource oriented programming. Resource oriented programming is that act of applying RESTful concepts to programming, so that resources can be treated with a uniform interface. Reactivity fits well within this concept because it is intended to provide a uniform interface for responding to changes in resources, using the observer pattern. With this uniform approach, bindings can easily and naturally be written.

Viewed from this perspective, REST suggests a surprisingly insightful mechanism for reactivity. A key mechanism in REST is caching, the semantics of resource interfaces are driven by exposing how resources can be cached. This resource-oriented approach can guide reactivity, but first, let’s review the mechanism of reactivity.

Generally, reactivity is built on an interface for adding a listener to be notified when a particular value changes, often called the observer pattern. This is sometimes compared to a continuous promise. A promise is something that be resolved and any listeners notified. A reactive, observable value is similar, except that the rather having a single resolution, it can continue to resolve to different values indefinitely. Alternately, reactivity is sometimes compared to a stream, with new values being streamed to a listener as they come in (there is some impedance mismatch with streams though, since reactivity differs in that listeners may not care about the entire sequence, only the last value).

Perhaps the most obvious strategy for these change notifications in the observer pattern, is to include the changed value in the notification. With this approach, when a change occurs, the new values are propagated out to any listeners, as part of the notification. A consumer would retrieve an initial value, and then listen for changes and respond to them.

The REST-inspired resource-oriented approach still uses the observer pattern to alert consumers of changes. However, with the REST approach, the notifications and flow of data are delineated. Change events function as simply invalidation notifications, and the flow of data always takes place as a separate request from the consumer to the source.

Invalidation notifications complements caching. Caching of values (particularly computed values) can be employed to ensure that no unnecessary computations take place, and any entity can perform caching safely with the knowledge that we will be notified if any of the source data is invalidated.

The REST approach of keeping notifications (only responsible for indicating a change) separate from a cacheable data request mechanism, rather than using notifications as the data delivery mechanism has several important advantages:

  • Consumers always follow the same code path to get data and act on it. Regardless of whether consumption (like rendering data) is on the initial retrieval, or whether it is triggered from a change notification, the consumer always requests and retrieves data in the same, consistent way.
  • Consumers have control over whether or not to actually request data in response to a notification. There can be plenty of situations where a consumer may temporarily or permanently not need to access new data when notified of its presence. For example, a component that is currently hidden (think of an object in pane of an unselected tab), may not need to force all the computations necessary to retrieve the data, if it doesn’t actually need to render it. The notification of a change may be important to be aware that it will need to rerender if and when it is visible again, but it could potentially ignore multiple data changes while hidden, by simply not retrieving data until necessary. This approach affords maximum laziness in applications.
  • When consumers retrieve data, it produces more coherent call stacks. Call stacks that consist of data sources calling intermediaries calling consumer components can be incredibly confusing to track. While data sources can still be at the bottom of the stack with their notifications, the actual data flow will always be ordered by consumers calling intermediaries calling data sources, for consistent, intuitive debugging.

Interestingly, EcmaScript’s new Object.observe functionality, which provides native object notifications, actually follows this same approach. The notification events that are triggered by property changes, are only invalidation notifications, that do not include the changed value. This aligns with the resource-oriented approach I have described, and encourages this strategy.

There are certainly situations where this approach is not appropriate. In particular, this isn’t appropriate when there are large latency increases associated with separating notification and retrieval operations (for example, if they necessitate extra network requests). However, within a single process, I believe this resource-oriented cache-and-invalidate approach can yield cleaner and more efficient code, and is worth considering as you approach reactive programming.

Advertisements