
What is reactive programming?
The Wikipedia article teaches that this is a programming paradigm that
focuses on data flow and propagation of changes . This definition, though technically correct (of course!), Gives an extremely vague idea of ​​what really hides behind all this. Meanwhile, the concept of reactivity is simple and natural, and it is best explained using the following example.
We all have ever used spreadsheets like Microsoft Excel. In the cell of the table, the user can write a formula that refers to other cells. If the value of any of them changes, the formula will be recalculated, and our cell will automatically update. At the same time, if our cell participates in other formulas, then they will be automatically recalculated, and so on, and so on - a process resembling the development of a chain reaction. So, this is the main idea of ​​reactive programming!
On Habré, there were already quite a few articles on the topic of reactive programming (
one ,
two ,
three ,
four, and others) - mainly they describe reactivity in their
FRP- apostles in the form of libraries like
bacon.js for JavaScript,
JavaRx for Java, etc. . This article will discuss the implementation and application of reactive programming in the
Jancy language. The material will be interesting to read, even if you have never heard of Jancy and do not intend to write anything on it - because we will demonstrate a rather unusual approach to reactivity from an imperative language.
Whichever of the existing reactive libraries we take, the
“Observable” pattern will be key in the implementation. Indeed, in order to “propagate” the changes, we at least need to receive notification of them. Observable entities are usually divided into two primitives (they are called differently in different libraries):
- Event stream (EventStream / Observable / Event / Stream);
- Properties (Property / Behavior / Attribute) - a value that changes with time.
In FRP, all this is insisted on the elements of
functional programming : to construct complex structures of such primitives, functions of higher order are used such as map, reduce, filter, combine, etc. which generate secondary streams and events (instead of “modifying” the original ones). The resulting compote of observables and functionalism is not so difficult to understand and master, and at the same time allows us to express the dependencies between components in a
declarative form . This is great for programming intricate user interfaces and distributed asynchronous systems. So is there anything to improve?
')
Problems
The first problem is this. If reactivity is implemented at the library level, without observables in the compiler, then
automatically recalculated formulas a la Excel remain an unattainable ideal. Instead, you have to manually make several map and combine over our observables - the more, the more complex the logic of our formula, and then onValue / assign to write the resulting value to the right place.
Perhaps
Flapjax , an open source JavaScript compiler, came closest to Excel-like formulas (if there are other projects of this kind, please write about them in the comments). Type 2 observables, which are called Behavor in Flapjax, can be arbitrarily combined in expressions and get new Behavor output.

But there is another fundamental problem that is inherent in both reactive libraries, and Flapjax - this is the problem
“do not boil the pot” . After we have created our infrastructure from streams of events, properties, and reciprocal subscriptions to each other, it begins to live its own life. The data flows and is transformed, as we asked them, the necessary actions are performed in all onValue and onCompleted, everything is great. So, how to stop it now? Run through all root observables and stop the issue of events manually? Already not very beautiful. And what if it is necessary to stop not everything, but only a part of our reactive dependency graph? Given that the lion's share of our observables exists in the form of implicit results map / combine / filter?
If we reformulate it somewhat differently, then one of the problems with existing reactive libraries is that (largely due to their functional orientation) they generate a single-level structure of observable objects!
However, it is always easier to criticize than to offer some alternative. So what does Jancy boast in terms of reactivity?
- Excel-like automatic recalculation of formulas with observables - and only where the programmer chooses;
- The ability to group dependency clusters between observables - and then start and stop all subscriptions in the cluster at once.
It looks like this:
reactor TcpConnectionSession.m_uiReactor () { m_title = $"TCP $(m_addressCombo.m_editText)"; m_isTransmitEnabled = m_state == State.Connected; m_adapterProp.m_isEnabled = m_useLocalAddressProp.m_value; m_localPortProp.m_isEnabled = m_useLocalAddressProp.m_value; m_actionTable [ActionId.Connect].m_text = m_state ? "Disconnect" : "Connect"; m_actionTable [ActionId.Connect].m_icon = m_iconTable [m_state ? IconId.Disconnect : IconId.Connect]; m_statusPaneTable [StatusPaneId.State].m_text = m_stateStringTable [m_state]; m_statusPaneTable [StatusPaneId.RemoteAddress].m_text = m_state > State.Resolving ? m_remoteAddress.getString () : "<peer-address>"; m_statusPaneTable [StatusPaneId.RemoteAddress].m_isVisible = m_state > State.Resolving; }
This is a squeeze from the source of the session TCP Connection Terminal
IO Ninja . As it is easy to guess, this code is engaged in updating the UI with changes in status, text in the combo box, etc.
And now about how it works.
General plan
First of all, in order to avoid confusion, we will agree on terminology.
The property (property) in Jancy has a generally accepted (non-reactive) definition - this is a kind of thing that looks like a variable / field, but at the same time allows you to perform actions in accessor functions.
Multicasts (multicast) and
events (event) are used to accumulate pointers to functions and call them all at once (the differences between multicasts and events a little later).
In Jancy, only one type of observable at the compiler level is the
“ bindable property”, i.e. a property that can notify about its change through the onChanged event.
Unlike analogs, reactivity in Jancy does not try to be “too smart” and go everywhere where observables are used — with side effects like automatic subscription, implicit generation of new observables, etc. She stands in the corner and is not asking. Access to associated properties in an imperative style is no more than access to a regular variable.
How does this combine with the above-mentioned reactive recalculation a la Excel? The conflict-free coexistence of reactive and imperative beginnings in Jancy is possible because there are
special zones of reactive code - the so-called.
reactors (reactors). Instead of a sequence of instructions, reactors consist of Excel-like formulas - expressions, each of which must use associated properties. Inside the reactors, the properties to be bound behave “reactively”.
So, the main building blocks of which reactive programming in Jancy is built are
events ,
associated properties and
reactors . Consider these bricks closer.
Multicasts and events
The multicast (multicast) in Jancy is a compiler-generated special class that allows you to accumulate function pointers and then call them all at once. A multicast declaration is very similar to a function declaration, which is not surprising since it must uniquely determine which type of function pointers will be stored in this multicast:
foo (int x); bar ( int x, int y ); baz () { multicast m (int);
Learn more about multicast class methods.For example, let's define a simple multicast:
multicast m (int);
The multicast class generated in the example above will have the following methods:
void clear (); intptr set (function* (int));
The set and add methods return a certain integer cookie that can be used in the remove method to effectively remove a pointer from a multicast.
Some of the methods also have aliases in the form of statements:
multicast m (); m = foo;
Multicast can be led to a pointer to a function that will cause all the pointers accumulated in the multicast. But there is ambiguity, namely: should such a casting be “live” (live) or snapshot? In other words, if after creating a pointer to a function, we modify the original multicast, should this pointer see changes?
To resolve ambiguity multicashes provide a getSnapshot method that returns a snapshot. At the same time, the cast operator gives a “live” pointer:
foo (); bar (); baz () { multicast m (); m += foo; function* f1 () = m.getSnapshot (); function* f2 () = m; m += bar; f1 (45);
Events (
events ) in Jancy are special
pointers to multicasts
with access restriction : you can only do add and remove:
foo () { multicast m (int); event* p (int) = m; p += bar;
Declaring a variable or field of type "event" creates a
dual type : for "friends", this type behaves as if the multicast modifier were used, and for "alien" it is an event with the prohibition of calling all methods except add and remove.
Read more about dual types in JancyThe main difference between the access model in Jancy and most other object-oriented languages ​​is the reduction of the number of access specifiers to two -
public and
protected .
On the one hand, it provides developers with significantly less flexibility in determining who has access to what. On the other hand, this simplified model makes it possible to clearly divide everyone into
“friend or foe” , and this, in turn, opens up the possibility of
dual modifiers , i.e. modifiers that have different meanings for "their" and "alien", and the
dual types created with their help.
So, in Jancy, for each individual namespace A, the rest of the world falls into two categories: “own” and “alien”. In addition to the namespace A itself, its “owns” include:
- Namespaces of classes or structures inherited from A;
- Namespaces declared as friend (friend);
- Children in relation to A namespaces;
- Extensions (extension namespaces) A.
All others are "alien". "Own" have access to both public (public) and protected (protected) members of the namespace, while "alien" - only to public members. In addition, belonging to the group of "their" or "alien" changes the meaning of Jancy dual modifiers.
The
readonly dual modifier can be used for elegant read-only access. Instead of writing trivial getters, the only purpose of which would be access control, a Jancy developer can declare fields with a readonly modifier. For "their", the readonly modifier seems to be invisible, for the "alien" readonly is treated as
const :
class C1 { int readonly m_progress; foo () { m_progress += 25;
The main advantage of this approach is that it makes the code shorter and more natural; As a side effect, you can call a simplification, and therefore an acceleration of the work of the optimizer, which does not need to analyze and discard dummy getters.
The second dual modifier in Jancy is
event . The event owner must have complete control over it, including the ability to call all subscribers or clear their list. The event client should only be able to add or remove a subscriber. For "their" field with the event modifier works the same way as a multicast with the corresponding signature of the arguments. For "alien", this field restricts access to multicast methods: only add and remove calls are allowed; call, set, clear, getSnapshot and casting to a pointer-to-function are prohibited:
class C1 { event m_onCompleted ();
Properties
In the context of programming languages, a
property is something that looks and behaves like data, but at the same time allows you to perform some additional actions when reading and writing. Without false modesty, Jancy provides the most comprehensive to date implementation of the properties of any shape, color and size.
More and with examplesDefinitions
Functions that perform actions in reading and writing are called
accessors : the property reading accessor is called a getter, and the writing is called a setter.
Each property in Jancy has one
getter and optionally one or several (overloaded)
setters (that is, there is no write-only property in Jancy). If the setter is overloaded, then the selection of a specific setter will be made while assigning a value to a property according to the same rules that select the overloaded function.
If the property does not have a setter, then it is called
constant (const-property). In other programming languages, properties without setters are usually called read-only, but since Jancy's
const and
readonly coexist (readonly is a dual modifier), it would have to override the well-known definitions one way or another. So, in Jancy, a property without a setter is a const property.
Simple properties
For simple properties without overloaded setters (to which most practical tasks come down) the most natural form of declaration is proposed:
int property g_simpleProp; int const property g_simpleConstProp;
This form is ideal for declaring interfaces, or if the developer prefers the C ++ style of posting declarations and method implementations:
int g_simpleProp.get () {
Full form ad
For properties of arbitrary complexity (i.e., properties with overloaded setters, data fields, auxiliary methods, etc.) there is a complete form of the declaration:
property g_prop { int m_x = 5;
Indexed properties
Jancy also supports indexable properties, i.e. properties with array semantics. Accessors of such properties take additional index arguments. However, unlike real arrays, index arguments of properties are not required to be integer, and, strictly speaking, they do not have to have the meaning of an “index” at all - their use is completely determined by the developer:
int indexed property g_simpleProp (size_t i); property g_prop { int get ( size_t i, size_t j ); set ( size_t i, size_t j, int x ); set ( size_t i, size_t j, double x ); } foo () { int x = g_simpleProp [10]; g_prop [x] [20] = 100; }
Autoget properties
In the overwhelming majority of cases, the getter simply has to return the value of some variable or field where the current value of the property is stored, and the actual behavior logic of the property is embodied in the setter. Obviously, the creation of such trivial getters can be passed on to the compiler - as was done in Jancy. For autoget properties, the compiler automatically creates a getter and a field for storing data. Moreover, the compiler generates direct access to the field, bypassing the getter, wherever possible:
int autoget property g_simpleProp; g_simpleProp.set (int x) { m_value = x;
The opposite situation, when the special logic of the behavior of the property is embedded in the getter, and the empty setter must simply put the data in a memory cell, is rare and does not deserve the creation of a special syntax.
When applied to reactive programming, the
properties of interest are of greatest interest, properties that can notify subscribers of their changes. As you can guess, Jancy uses the
multicast / event mechanism to implement the associated properties:
int autoget bindable property g_simpleProp; g_simpleProp.set (int x) { if (x == m_value) return; m_value = x; m_onChanged ();
For access to the events notifying on changes of the bound properties, the operator is used
bindingof :
onSimplePropChanged () {
Jancy also supports binding properties with fully compiled accessor accessors — both the getter and the setter. These kind of degenerative properties are called bindable data. They serve the only purpose - to catch the moment of change - and can act as simple observable variables / fields:
int bindable g_data; onDataChanged () {
Reactors
The reactor in Jancy is a zone of a reactive code. All reactive dependencies and implicit subscriptions are located inside the reactors.
Externally, the reactor looks like a normal function, except that in the declaration specified modifier reactor. Unlike functions, each reactor creates a variable or field of a special
reactor class with two public methods: start and stop, which allow starting and stopping the reactor. Instead of statements (statements) that make up the body of a normal function, the reactor body consists of a sequence of expressions, each of which should use in its right side the associated properties:
State bindable m_state; reactor m_uiReactor () { m_isTransmitEnabled = m_state == State.Connected; m_actionTable [ActionId.Disconnect].m_isEnabled = m_state != State.Closed;
At start-up, the reactor builds a dependency graph and subscribes to all associated events of all “controlling” properties. When changing any of them, all dependent expressions are recalculated (which, of course, can cause an avalanche change in other properties).
And what to do with cyclical dependencies?At the moment, cyclic dependency updates are simply ignored - that is, if any of the control properties of P changed and caused a recalculation of expressions in the reactor, which in turn changed the value of this property P, then this repeated change will not cause recursive calculations in the reactor.
In the future, most likely, the reactors will have the settings for the permissible recursion depth and recovery strategy, if the depth is still exceeded (ignore / stop the reactor with an error / call some callback / etc.)
In addition to reactive expressions, arbitrary events and their processing code can be linked in reactors in an intuitive syntax. For this, the construction of
onevent . This approach allows you to use the traditional event approach to the UI and at the same time eliminates the need to subscribe to events manually:
reactor m_uiReactor () { onevent m_startButton.m_onClicked () {
At shutdown, the reactor unsubscribes from all events to which it is subscribed (if the reactor is a member of the class, then the shutdown automatically occurs at the moment of destruction of the parent object). Thus, the developer has the ability to determine in detail and
where to use the reactive approach (reactor zones), and
when (start / stop). All implicit subscriptions are gathered together and it is very easy to make a sacramental
"pot not boiling" :
m_uiReactor.stop ();
In this case, of course, there may be several reactors that use the same properties to be linked and other events - if this is required for the logical grouping of dependencies into certain clusters.
Putting it all together
So, we have all the cubes in order to collect beautiful user interface frameworks from them and use them in a reactive style:
Multiple UI Classes class Widget { bitflag enum SizePolicyFlag { Grow, Expand, Shrink, Ignore, } enum SizePolicy { Fixed = 0, Minimum = SizePolicyFlag.Grow, Maximum = SizePolicyFlag.Shrink, Preferred = SizePolicyFlag.Grow | SizePolicyFlag.Shrink, MinimumExpanding = SizePolicyFlag.Grow | SizePolicyFlag.Expand, Expanding = SizePolicyFlag.Grow| SizePolicyFlag.Shrink | SizePolicyFlag.Expand, Ignored = SizePolicyFlag.Shrink | SizePolicyFlag.Grow | SizePolicyFlag.Ignore } protected intptr m_handle; SizePolicy readonly m_hsizePolicy; SizePolicy readonly m_vsizePolicy; setSizePolicy ( SizePolicy hpolicy, SizePolicy vpolicy ); bool autoget property m_isVisible; bool autoget property m_isEnabled; } opaque class Label: Widget { bitflag enum Alignment { Left, Right, HCenter, Justify, Absolute, Top, Bottom, VCenter, } char const* autoget property m_text; int autoget property m_color; int autoget property m_backColor; Alignment autoget property m_alignment; Label* operator new (char const* text); } opaque class Button: Widget { char const* autoget property m_text; event m_onClicked (); Button* operator new (char const* text); } opaque class CheckBox: Widget { char const* autoget property m_text; bool bindable property m_isChecked; CheckBox* operator new (char const* text); } opaque class TextEdit: Widget { char const* property m_text; TextEdit* operator new (); } opaque class Slider: Widget { int autoget property m_minimum; int autoget property m_maximum; int bindable property m_value; Slider* operator new ( int minimum = 0, int maximum = 100 ); }
Their use from the reactor Slider* g_redSlider; Slider* g_greenSlider; Slider* g_blueSlider; int bindable g_color; Label* g_colorLabel; CheckBox* g_enablePrintCheckBox; TextEdit* g_textEdit; Button* g_printButton; int calcColorVolume (int color) { return (color & 0xff) + ((color >> 8) & 0xff) + ((color >> 16) & 0xff); } reactor g_uiReactor () { g_color = (g_redSlider.m_value << 16) | (g_greenSlider.m_value << 8) | (g_blueSlider.m_value); g_colorLabel.m_text = $"#$(g_color;06x)"; g_colorLabel.m_backColor = g_color; g_colorLabel.m_color = calcColorVolume (g_color) > 0x180 ? 0x000000 : 0xffffff; g_textEdit.m_isEnabled = g_enablePrintCheckBox.m_isChecked; g_printButton.m_isEnabled = g_enablePrintCheckBox.m_isChecked; onevent g_printButton.m_onClicked () { printf ($"> $(g_textEdit.m_text)\n"); } }
Dear habrovchane are invited to the
live page of our compiler, to test the reactive capabilities of Jancy without the need to download, install or build anything.
Those who want to download and compile / just delve into the source Jancy, can do it from the
download page . By the way, in the samples / 02_dialog folder is the above example of hanging reactivity on QT widgets.
And the real use of Jancy and its reactive capabilities can be viewed in our programmable terminal /
IO Ninja sniffer.