Skip to content
schlangster edited this page May 24, 2014 · 1 revision

Motivation

Let's get started with a basic example that explains the motivation behind signals and which kind of problems they are meant to address.

The problem

Here's a class Shape with two dimensions Width and Height that can be changed imperatively:

class Shape
{
public:
    int Width  = 0;
    int Height = 0;
};

The size of the shape should be calculated accordingly:

int calculateSize(int width, int height) { return width*height; }

Solution 1: Simple member function

class Shape
{
public:
    int Width  = 0;
    int Height = 0;
    int Size() const { return Width * Height; }
};

This gets the job done, but whenever Size() is called, the calculation is repeated, even if the the shape's dimensions did not change after the previous call. With this simple example it would be fine, but let's assume calculating size would be an expensive operation. We rather want to re-calculate it once after width or height have been changed and just return that result in Size().

Solution 2: Manually triggered re-calculation

class Shape
{
public:
    int Width() const   { return width_; }
    int Height() const  { return height_; }
    int Size() const    { return size_; }

    void SetWidth(int v)
    {
        if (width_ == v) return;
        width_ = v;
        updateSize(); 
    }
    
    void SetHeight(int v)
    {
        if (height_ == v) return;
        height_ = v;
        updateSize();
    }

private:
    void updateSize()   { size_ = width_ * height_; }

    int width_  = 0;
    int height_ = 0;
    int size_   = 0;
};
Shape myShape;

// Set dimensions
myShape.SetWidth(20);
myShape.SetHeight(20);

// Get size
auto curSize = myShape.Size();

To calculate the size, updateSize() is called manually after width or height have been changed.

This adds quite a bit of boilerplate code and as usual when having to do things manually, we can make mistakes.

What if more dependent attributes should be added? Using the current approach, updates are manually triggered from the dependencies. This requires changing all dependencies when adding a new dependent values, which gets increasingly complex. More importantly, it's not an option, if the dependent values are not known yet or could be added and removed dynamically. A common approach to enable this are callbacks.

Solution 3: Callbacks

class Shape
{
public:
    using CallbackT = std::function<void(int)>;

    int Width() const   { return width_; }
    int Height() const  { return height_; }
    int Size() const    { return size_; }

    void SetWidth(int v)
    {
        if (width_ == v) return;
        width_ = v;
        updateSize(); 
    }
    
    void SetHeight(int v)
    {
        if (height_ == v) return;
        height_ = v;
        updateSize();
    }

    void AddSizeChangeCallback(const CallbackT& f)   { sizeCallbacks_.push_back(f); }

private:
    void updateSize()
    {
        auto oldSize = size_;
        size_ = width_ * height_;
        
        if (oldSize != size_)
            notifySizeCallbacks();
    }

    void notifySizeCallbacks()
    {
        for (const auto& f : sizeCallbacks_)
            f(size_);
    }

    int width_  = 0;
    int height_ = 0;
    int size_   = 0;

    std::vector<CallbackT> sizeCallbacks_;
};
Shape myShape;

// Callback on change
myShape.AddSizeChangeCallback([] (int newSize) {
    redraw();
});

For brevity, this example includes callbacks for size changes, but not for width and height. Nonetheless, it adds even more boilerplate. Instead of implementing the callback mechanism ourselves, we can use external libraries for that, for example boost::signals2, which handles storage and batch invocation of callbacks; but overall, it has no impact on the design.

To summarize some of the pressing issues with the solutions shown so far:

  • Error-proneness: There is no guarantee that size == width * height. It's only true as long as we don't forget to call updateSize() after changes.
  • Boilerplate: Check against previous value, trigger update of dependent internal values, trigger callback notification, register callbacks, ...
  • Complexity: Adding new dependent attributes requires changes in existing functions and potentially adding additional callback holders.
  • API pollution: Registration functions for callbacks.

What it boils down to is that the change propagation must be handled by hand. The next example shows how features of our library address those issues.

Final solution: Using react::Signal

#include "react/Domain.h"
#include "react/Signal.h"
#include "react/Observer.h"

using namespace react;

REACTIVE_DOMAIN(D);

class Shape : public ReactiveObject<D>
{
public:
    VarSignalT<int> Width   = MakeVar(0);
    VarSignalT<int> Height  = MakeVar(0);
    SignalT<int>    Size    = Width * Height;
};
Shape myShape;

// Set dimensions
myShape.Width <<= 20;
myShape.Height <<= 20;

// Get size
auto curSize = myShape.Size(); // Or more verbose: myShape.Size.Value()

Size now behaves like a pure function of Width and Height similar to Solution 1. But behind the scenes, it still does everything it does in Solution 2, i.e. only re-calculate size when width or height have changed.

Every reactive value automatically supports registration of callbacks (they are called observers here):

// Callback on change
myShape.Size.Observe([] (int newSize) {
    redraw();
});

// Those work, too
myShape.Width.Observe([] (int newWidth) { ... });
myShape.Height.Observe([] (int newHeight) { ... });