-
Notifications
You must be signed in to change notification settings - Fork 131
Reactive Objects
The tutorials in the Event and Signal guides defined reactives in a global context, but in practice the predominant use case should be defining them as class members. There is nothing fundamentally different about that.
The following examples use this domain definition:
#include "react/Domain.h"
REACTIVE_DOMAIN(D);
We revisit the initial example from the signal guide:
#include "react/Signal.h"
#include "react/Event.h"
class Shape
{
public:
D::VarSignalT Width = D::MakeVar(0);
D::VarSignalT Height = D::MakeVar(0);
D::SignalT Size = width * height;
D::EventSourceT<> HasMoved = D::MakeEventSource();
};
Qualifying the domain name before every type and function is tedious. Inside of class definitions, it can be avoided:
#include "react/Signal.h"
#include "react/Event.h"
#include "react/ReactiveObject.h"
class Shape : public ReactiveObject<D>
{
public:
VarSignalT width = MakeVar(0);
VarSignalT height = MakeVar(0);
SignalT size = width * height;
EventSourceT<> HasMoved = MakeEventSource();
};
By extending ReactiveObject<D>
, the class inherits type aliases and wrapper functions for the given domain.
Providing these aliases is all ReactiveObject
does.
We used C++11 in-class member initialization. Alternatively, we could've initialized the signals in the constructor: For both variants, it's important to mind the initialization order:
class Shape : public ReactiveObject<D>
{
public:
VarSignalT Width;
VarSignalT Height;
SignalT Size;
EventSourceT<> HasMoved;
Shape() :
// Things break because Width and Height are still unitialized at this point
Size{ Width * Height },
Width{ MakeVar(0) },
Height{ MakeVar(0) },
HasMoved{ MakeEventSource() }
{}
};
class Company
{
public:
const char* Name;
Company(const char* name) :
Name{ name }
{}
};
class Employee : public ReactiveObject<D>
{
public:
VarSignalT<Company&> MyCompany;
Employee(Company& company) :
MyCompany{ MakeVar(std::ref(company)) }
{}
};
Company company1{ "MetroTec" };
Company company2{ "ACME" };
Employee bob{ company1 };
bob.MyCompany.Observe([] (const Company& company) {
std::cout << "Bob works for " << company.Name << std::endl;
});
bob.Company <<= std::ref(company2); // output: Bob now works for ACME
As shown, input to a signal of a reference has to be wrapped by std::ref
or std::cref
to make the reference obvious.
Event streams of references are used in the same fashion.
Continuing from on the previous tutorial, consider that the company name is not an immutable string, but a signal as well:
#include <string>
using std::string;
class Company : public ReactiveObject<D>
{
public:
VarSignal<string> Name;
Company(const char* name) :
Name{ name }
{}
};
Company company1{ "MetroTec" };
Company company2{ "ACME" };
Employee alice{ company1 };
We want to create an observer of the name of Alice's company, instead of the company itself like before. This might work:
Observe(
alice.MyCompany.Value().Name,
[] (const string& name) {
std::cout << "Alice works for " << name << std::endl;
});
But the following input reveals a problem:
company1.Name <<= string("ModernTec"); // output: Alice now works for ModernTec
// OK so far
alice.Company <<= std::ref(company2); // no output
// Name should've changed
The observer was registered to the name of the company Alice worked for at the time, as indicated by MyCompany.Value()
.
When the company changes from company1
to company2
, it has to shift from company1.Name
to company2.Name
.
This is enabled by the REACTIVE_REF
macro:
D::SignalT<string> myCompanyName = REACTIVE_REF(alice.MyCompany, Name);
Observe(myCompanyName, [] (const string& name) {
std::cout << "Alice works for " << name << std::endl;
});
The intermediate signal can be avoided, but then the observer handle has to be kept in scope instead:
D::ObserverT obs = Observe(
REACTIVE_REF(alice.MyCompany, Name),
[] (const string& name) {
std::cout << "Alice works for " << name << std::endl;
});
Otherwise, the lifetime of the observer would be tied to a temporary signal, which would be destroyed immediately after construction, together with the former.
In both cases, the output is now:
company1.Name <<= string("ModernTec"); // output: Alice now works for ModernTec
alice.Company <<= std::ref(company2); // output: Alice now works for ACME
company2.Name <<= string("A.C.M.E."); // output: Alice now works for A.C.M.E.
A similar macro REACTIVE_PTR
exists for pointer types instead of references (i.e. VarSignalT<Company*> Company
).
TODO