Declarative approach is known to be very useful in some aspects of software development. In contrast to the typical imperative way of thinking it promotes to design software from top to bottom, from final result back to simple separate steps.
For example, assume one needs to create an interactive OpenGL image view component with the possibility of real-time image processing. Let's start from the result desired, i.e. from the final image rendering on screen. The only prerequisites are the transformation matrix and the OpenGL texture of the processed image. One can already write this part of code, using (yet non-existent) sources of these two components. The transformation matrix is a combination of user induced translation, zoom factor and geometrical transformations. The OpenGL texture depends on the initial image and processing settings, etc. The initial image itself is just a function of filename. And so on following the dependency graph.
Connecting such building blocks from top to bottom not only makes life easier for the developer, but also allows us to change such blocks later. For example, we can easily replace the image source with a real-time image from a camera without touching existing code. Or one can refresh filenames 60 times per second and get a playback of processed video with user interaction.
This is how the software is usually designed in functional programming languages — everything is just a combination of various functions. Note that deeper level functions are lazy by default and computed only when their results are needed, namely during execution of "effects". Interactivity can be added here by implementing a notification conveyor to inform higher levels of the function graph about changes.
For our everyday work we developed a library called "the Flow". The base building blocks are "sources" which produce value and notify customers about their changes. The sources can be easily combined together and used like input arguments for other sources. All notifications are spread automatically through the whole computation graph. As written above they are lazy by default and are only computed upon request.
This approach is somewhat similar to Qt properties but encapsulates both the value and notification source in one shell which makes it much simpler to combine them together and pass them on over the application. No more manual "update" calls and no need to connect signal and slots manually. This approach will be familiar to React users with the difference that Flow is based on current state and not on the data flow.
The library also has several other convenient abilities. In particular, it has built-in lifetime control so the source will remain alive while it's used by other consumers. We use std::shared_ptr in C++, C# is naturally designed for this.
The consumer of long-lived sources don't have to worry about subscriptions/unsubscriptions, everything is done automatically. This helps to exclude the possibility of quite typical memory leaks. Furthermore, it is guaranteed that the object will remain alive until the last notification callback finishes execution. We use std::weak_ptr in C++, C# version use ConditionalWeakTable. One can still manually switch between several input sources or even disconnect the consumer by using special connector components (read-only or even mutable).
Flow tends to use immutable structures instead of single values where applicable. If several parameters are of interest as a whole structure this approach is preferable over single properties because it reduces the amount of code and number of connections.
The library preserves Qt thread affinity and provides native thread-safety. The changes to any source can be safely made from any thread without thinking about locks. All actions and triggers ("effects'' in FP) are automatically executed in a proper thread. One can also explicitly specify on which thread the computation must be run or even do it in the background. This is useful in many cases including interaction with OpenGL which is naturally single threaded.
Routines executed in other threads can throw exceptions accidentally. Flow catches all such situations and forwards them to a customizable central method of exception processing. Of course, one can catch any unexpected situation manually and process it in situ.
Apart from laziness Flow uses smart caching of computation results. It's easy to implement a custom cache limiter which can be fine-tuned for different usages. For example, one can limit the amount of RAM to cache numerical results and use separate policy for GPU textures.
There are several helpers out of the box. Converters can be used for two-way conversion of values forth and back, throttlers flexibly reduce on-change event intensity, barriers can prevent update during multiple input parameter changes, etc.
Flow is used in all our projects and shipped under LGPL v3.0 license.