MetaRx User Manual v0.1.7

Reactive programming

Reactive programming is a paradigm that is concerned with:

Data propagation

Concretely, a data structure is said to be reactive (or streaming) if it models its state as a stream. It does this by defining change objects (also called deltas) and mapping its operations onto these. The published stream is read-only and can be subscribed. If the stream does not have any subscribers, the state does not get persisted and is lost.

Example: A reactive list implementation could map all of its operations like append(), insertAfter(), delete() and clear() on only two delta types, namely Insert and Delete. A subscriber can interpret the delta objects and persist the computed list in an in-memory buffer.

A reactive data structure can also stream state observations. Sticking to the reactive list example, we could stream observations based on the list’s inherent properties — one being its current length, another the existence of a certain element, i.e. contains(value).

Finally, a mutable reactive data structure is an extension with the sole difference that it maintains an internal state which always represents the computed result after a delta was received. This is a hybrid solution bridging mutable object-oriented objects with reactive data structures.

The mutable variant of our reactive list could send its current state when a subscriber is registering. Subscribers can register at any point without caring whether the expected data has been propagated yet already. The second motivation for a mutable reactive data structure is that they reduce the overall memory usage; otherwise we would need multiple instances of mutable objects that interpret the deltas.

Data flows

For now we have just covered the first component of reactive programming: data propagation. The second cornerstone, data flow, is equally important though. Streams describe data flow in terms of dependencies. Considering you want to plot a line as a graph using the formula y = mx + b while the user provides the values for m and b, then you would wrap these inputs in channels and express the dependencies using combinators[1]:

val m: Var[Int] = Var(5)
val b: Var[Int] = Var(10)

// Produces when user provided `m` and `b`
val mAndB: ReadChannel[(Int, Int)] = m.zip(b)

// Function channel to calculate `y` for current input
val y: ReadChannel[Int => Int] =
  mAndB.map { case (m, b) =>
    (x: Int) => m * x + b
  }

// Render `y` for inputs [-5, 5]
val values: ReadChannel[Seq[Int]] = y.map((-5 to 5).map(_))
values.attach(println)

// Simulate change of `m` in UI
m := 10
Output:
Vector(-15, -10, -5, 0, 5, 10, 15, 20, 25, 30, 35)
Vector(-40, -30, -20, -10, 0, 10, 20, 30, 40, 50, 60)

In values we listen to y and whenever it receives a new function, it calls it for all the x in the interval of the shown graph. The example shows that messages in streams are not bound to data objects and even immutable functions could be passed around.

The data propagation from the example is illustrated by the following diagram:

As soon as the user inserts a new value for m or b, mAndB will produce a tuple. Then, y computes the final function.

How channels work in detail is explained in the following sections. This example should only give an intuition of the fundamental concept as well as how data dependencies are expressed.

Streams

The term "stream" was used several times in this chapter. This term is polysemous and requires further explanation. In reactive programming there are different types of streams with significant semantic differences.

Observables

Rx (Reactive Extensions) is a contract designed by Microsoft which calls streams observables and defines rules how to properly interact with them. An observable can be subscribed to with an observer which has one function for the next element and two auxiliary ones for handling errors and the completion of the stream.

Furthermore, observables are subdivided into cold and hot observables[2]:

Back-pressure

There are extensions to Rx which introduce back-pressure[3] to deal with streams that are producing values too fast. This may not be confused with back-propagation which describes those streams where the subscribers could propagate values back to the producer.

Implementation

This illustrates the diversity of streams. Due to the nature of MetaRx, streams had to be implemented differently from the outlined ones. Some of the requirements were:

To better differentiate from the established reactive frameworks, a less biased term than observable was called for and the reactive streams are therefore called channels in MetaRx. The requirements have been implemented as follows: A subscriber is just a function pointer (wrapped around a small object). A channel can have an unlimited number of children whereas each of the child channels knows their parent. A function for flushing the content of a channel upon subscription can be freely defined during instantiation[4]. When a channel is destroyed, so are its children. Error handling is not part of the implementation. Similarly, no back-pressure is performed, but back-propagation is implemented for some selected operations like biMap().

For client-side web development only a small percentage of the interaction with streams require the features observables provide and this does not justify a more complex overall design. It is possible to use a full-fledged model like Rx, Monifu or Akka Streams for just those areas of the application where necessary by redirecting (piping) the channel output.

Summary

To recap, a reactive data structure has four layers:

Obviously, the first three layers are the very foundation of object-orientation. It is different in that a) modifications are encoded as deltas and b) there are streaming operations.


  1. The types in the code are not needed for type inference and only serve illustration purposes.  ↩

  2. Source: leecampbell.blogspot.de (4th February 2015)  ↩

  3. For instance, Monifu implements this feature.  ↩

  4. This function is called by attach() and produces multiple values which is necessary for some reactive data structures like lists.  ↩

  5. These functions do not access the state in any way.  ↩

Generated with MetaDocs v0.1.1