Reactive programming is a paradigm that encompasses architectural and structural school of thought using a number of abstractions and constructs. The goal of the reactive paradigm is two-fold: i) to create responsive, scalable and fault-tolerant systems, and ii) to describe computations in terms of data-flows. The second point, implicitly, includes capturing temporal relationships amongst different events and operations. From an architectural perspective, a reactive system is a system which is
The two goals stated above are, in fact, related. Concurrent systems expressed in terms of data-flow are one of the most performant systems. A data-flow oriented system, execute operations in response to arrival of data and is not bound to execute ordered operations.
For any system there is a certain capacity at any certain moment. For example, what should a Web service do when it hits its maximum number of users? There are many ways to handle this situation, including failure which is lack of handling, throttling connections, queueing the connections and responding to them in a delayed manner, or dropping new connections until the load decreases. Another way to handle the situation is back-pressure. The component under stress gives feedback to the its users that they need to slow down. Although this might not be possible in all situations, for example, it makes no sense in an HTTP request-response scenario if each user issues a single request and interaction stops, but it makes sense when an HTTP client makes several requests or when the request is serviced by other components behind the HTTP server so that the feedback is propagated to the HTTP server. Back-pressure can still result in unresponsiveness if the feedback needs to be propagated to the end users of the component. However, the system is still resilient, it does not fail ungracefully and it does provide a mechanism for the whole system to be elastic.
Moving on to a more structural orientation, we discuss the following problem: given a text file in which lines are separated by the newline ASCII character and words are separated by the ASCII whitespace character, how can we go about counting unique words in this file? There are many ways to do this, some of which scale and some which do not. Some of which are easy to parallelise and some which are more involving. Let's consider two ways to solve this problem. The first is to have a map or a hash-table that maps each word to its count. The second way is to build an incremental pipeline, where each stage is responsible for solving a partial part of the problem and the output of the previous phase is the input to the next phase. The first phase is to emit a count of 1
for each word we encounter. The second phase takes these counts, combines counts for same words and reduces each combination to a tuple, such that (word, combined sum)
.
Describing the steps of these two algorithms in simple manner may be as follows. For the first algorithm:
The pipelined algorithm goes like this:
(word, 1)
tuple,(word, combined sum)
tuples.The pipeline algorithm may look foreign. However, first of all, it scales well. Second, it is rather simple and logical, we could describe it using a few keywords: split lines, split words, map words to 1, reduce counts. There are no conditional statements. Third, it is easy to parallelise. For the first algorithm we need to manually control the access to the shared hash-table using a mutex or lock or use a concurrent hash-table which atomically increments the value. The first method is not scalable and the second is too much for this application. Actually to implement such a data structure for billions of words across a cluster is non-trivial.
The second algorithm however could be simply parallelised. Generate the lines, let any free worker process a line to generate the words tuples, send the output to another free worker which knows how to reduce the counts for the same words. Note that each reducer worker knows how to take the input of other two combining workers and reduce them to tuples of word counts. Lastly, a final reducer produces the final answer. This scales to terabytes of texts simply and elegantly. We did not need to lock or think of parallelisation but only describe the computation in terms of a data-flow. That is the essence of functional programming and reactive programming. In fact, that is the essence of the MapReduce Framework as well. In reactive programming, in specific, we write a programme in terms of data-flows. How a data-flow is implemented is up to the framework to decide.
Graphically, the second algorithm looks like the following
Note that each line generates a sequence of words and we need these sequences all streamlined into a single sequence of words. This is why we used flat_map
instead of map
. These pipelines are called operators in reactive programming. Operators are constructs to describe or denote operations. One does not, usually, care how an SQL statement is implemented or how it executes, rather one cares about the functionality or the operation carried out denoted by the statement. A closer parallel are (pure) functions in a functional language that have no side-effects and whose output is completely specified by their input. In the same way, operators denote operations whose output is solely determined by their input. However, the programmer can, if they so desire, control how concurrently an operator executes as well.
Let's consider a second example, which will outline the core of the reactive programming paradigm. A statement as the following a = b + c
in an imperative programme means assign the sum of b
and c
to a
at this point in time. After the assignment takes place, b
and c
can be assigned different values without affecting the value of a
. However, in reactive programming, if a
is a reactive variable, whenever b
or c
changes, a
changes accordingly.
Implementing this relationship in a simple and crude way may result in the following:
class Constant:
def __init__(self, value, observers=None):
self._value = value
self._observers = observers if observers else []
@property
def value(self):
return self._value
@value.setter
def value(self, v):
self._value = v
for obs in self._observers:
obs.next(v)
def subscribe(self, observer):
self._observers.append(observer)
class Variable:
def __init__(self, fn, observables, observers=None):
self._fn = fn
self._observables = observables
self._value = None
self._observers = observers if observers else []
for var in observables:
var.subscribe(self)
@property
def value(self):
return self._value
def next(self, item):
self._value = self._fn(self._observables)
for obs in self._observers:
obs.next(v)
def subscribe(self, observer):
self._observers.append(observer)
def fn(variables):
return sum((v.value for v in variables))
b = Constant(1)
c = Constant(2)
a = Variable(fn, [b, c])
Although this implementation is rather metaphoric, it shows a number of interesting points. We describe the code and discuss interesting points as they arise. The Constant
class represents a value that can be observed by other objects. Whenever a constant's value changes, subscribed observers are notified of the new value. The Variable
class is also an observable class, however, it is also associated with a function, fn
, which is applied to a set of observables
to compute its own value. Hence, variable a
will change whenever b
or c
change. This is very similar to the Observer design pattern. The essence is that there is an object which other objects observe its state. In the Observer pattern the observed object is known as the subject. In reactive programming, it is known as observable.
An interesting point, which is critical in reactive programming, is the notion of time. In the listing above a
is equal to 0
in this specific code example. This is a point where different frameworks handle relative time in different ways. Some frameworks will initialise a
to 3
, the result of applying the function to its arguments, others to 0
and only whenever a change happens to an observed object, the reactive object changes accordingly.
Another point of interest is concurrency. If objects are related in such a deep manner, how to control concurrent events? Schedulers are used to control code execution, they provide the reference time for operations and maintain relative temporal relationships amongst operations. In reactive programming, the programmer has the choice to choose from many schedulers with different characteristics to execute different operations as they see fit.
At this point, one might notice that maintaining such temporal and executional relationships amongst many objects in a system leads to higher memory consumption. The question is whether it is justified. The remaining of the book discusses ways to build highly performant, scalable and fault-tolerant systems in simple manners using generic abstractions, which well justify some of the drawbacks of the paradigm in many contexts. Nonetheless, memory consumptions issues are subject of research. In reality, it is very crude to attach a list of observers to every object. Most likely, a reactive framework constructs a dependency graph that controls which operation executes at which time so that the state of the system holds consistent.
Reactive programming may be defined as programming objects distributed in time. Time can be modelled as continuous or discrete. The two models result in different implementations and abstractions. functional reactive programming is strictly defined as reactive programming in continuous time. However, in late years the term has also been used to mean realising the reactive paradigm on the basis of the functional paradigms, e.g. immutability and high-order functions, compostability, etc. In this book we will follow the discrete model and use functional programming to build reactive systems.