-
Notifications
You must be signed in to change notification settings - Fork 130
Signals
#include "react/Signal.h"
Contains the signal template classes and functions.
A signal is a reactive variable that can propagate its changes to dependents and react to changes of its dependencies.
Dataflow between signals is modeled as a directed acyclic graph, where each signal is a node and edges denote a dependency relation.
If an edge exists from v1
to v2
that means v1
will propagate its changes to v2
.
In other words, after a node changed, all its sucessors will be updated.
Here's a code example and the respective dataflow graph:
SignalT<int> a = ...;
SignalT<int> b = ...;
SignalT<int> c = ...;
SignalT<int> x = (a + b) * c;
The graph data structures are not directly exposed by the API; instead, instances of class Signal
(and its subtypes) act as lightweight proxies to nodes.
Such a proxy is essentially a shared pointer to a heap-allocated node, with additional interface methods depending on the concrete type.
Copy, move and assignment semantics are similar to std::shared_ptr
.
One observation taken from the previous example is that not all nodes in the graph are named signals; the temporary sub-expression a + b
results in a node as well.
If a new node is created, it takes shared ownership of its dependencies, because it needs them to calculate its own value. This prevents the a + b
node from disappearing.
The resulting reference graph is similar to the dataflow graph, but with reverse edges (and as such, a DAG as well).
This figure shows this for the previous example:
The number inside each node denotes its reference count. On the left are the proxy instances exposed to the API.
Assuming the handles for a
, b
and c
would go out of scope but x
remains, the reference count of all nodes is still 1, until x
disappears as well.
Once that happens, the graph is deconstructed from the bottom up.
Internally, each SignalT<S>
stores a value of type S
.
The general procedure that takes place when updating a signal is shown in the following:
void Update()
{
S newValue = evaluate(predecessors.Value() ...);
if (currentValue != newValue)
{
currentValue = std::move(newValue);
D::PropagationPolicy::OnChange(*this);
}
}
The propagation policy is responsible for calling Update
on successors.
It may not do so immediately, but order updates so that other predecessors that require updating in the same turn come first.
This allows to eliminate repeated re-calculations. It also ensures that all values of predecessors.Value()
are consistently from the current turn.
namespace react
{
template
<
typename D,
typename S
>
class Signal
{
public:
using ValueT = S;
// Constructor
Signal();
Signal(const Signal&);
Signal(Signal&&);
// Assignment
Signal& operator=(const Signal&);
Signal& operator=(Signal&& other);
// Tests if two Signal instances are equal
bool Equals(const Signal& other) const;
// Tests if this instance is linked to a node
bool IsValid() const;
// Returns the current signal value
const S& Value() const;
const S& operator()() const;
// Equivalent to react::Flatten(*this)
S Flatten() const;
};
}
S | Signal value type. Aliased as member type ValueT . |
Signal(); // (1)
Signal(const Signal& other); // (2)
Signal(Signal&& other); // (3)
(1) Creates an invalid signal that is not linked to a signal node.
(2) Creates a signal that links to the same signal node as other
.
(3) Creates a signal that moves shared ownership of the signal node from other
to this
.
As a result, other
becomes invalid.
bool Equals(const Signal& other) const;
Returns true, if both this
and other
link to the same signal node.
This function is used to compare two signals, because ==
is used as a combination operator instead.
bool IsValid() const;
Returns true, if this
is linked to a signal node.
const S& Value() const;
const S& operator()() const;
Returns a const reference to the current signal value.
template <class = std::enable_if<IsReactive<S>::value>::type>
S Flatten() const;
Semantically equivalent to the respective free function in namespace react
.
A variable signal extends the immutable Signal
interface with functions that support imperative value input.
namespace react
{
template
<
typename D,
typename S
>
class VarSignal : public Signal<D,S>
{
public:
// Constructor
VarSignal();
VarSignal(const VarSignal&);
VarSignal(VarSignal&&);
// Assignment
VarSignal& operator=(const VarSignal&);
VarSignal& operator=(VarSignal&& other);
// Set new signal value
void Set(const S& newValue);
void Set(S&& newValue);
// Operator version of Set
const VarSignal& operator<<=(const S& newValue);
const VarSignal& operator<<=(S&& newValue);
};
}
Analogously defined to constructor of Signal.
void Set(const S& newValue) const;
void Set(S&& newValue) const;
Set the the signal value of the linked variable signal node to newValue
. If the old value equals the new value, the call has no effect.
Furthermore, if Set
was called inside of a transaction function, it will return after the changed value has been set and change propagation is delayed until the transaction function returns.
Otherwise, propagation starts immediately and Set
blocks until it's done.
const VarSignal& operator<<=(const S& newValue);
const VarSignal& operator<<=(S&& newValue);
Semantically equivalent to Set
.
This class exposes additional type information of the linked node, which enables r-value based node merging at compile time.
TempSignal
is usually not used as an l-value type, but instead implicitly converted to Signal
.
namespace
{
template
<
typename D,
typename S,
typename TOp
>
class TempSignal : public Signal<D,S>
{
public:
// Constructor
TempSignal();
TempSignal(const TempSignal&);
TempSignal(TempSignal&&);
// Assignment
TempSignal& operator=(const TempSignal&);
TempSignal& operator=(TempSignal&& other);
};
}
Analogously defined to constructor of Signal.
namespace react
{
//Creates a new variable signal
VarSignal<D,S> MakeVar(V&& init);
// Creates a signal as a function of other signals
TempSignal<D,S,/*unspecified*/>
MakeSignal(F&& func, const Signal<D,TValues>& ... args);
// Creates a new signal by flattening a signal of a signal
Signal<D,T> Flatten(const Signal<D,Signal<D,T>>& other);
}
template
<
typename D,
typename V,
typename S = decay<V>::type,
>
VarSignal<D,S> MakeVar(V&& init);
Creates a new input signal node and links it to the returned VarSignal
instance.
template
<
typename D,
typename F,
typename ... TValues,
typename S = result_of<F(TValues...)>::type
>
TempSignal<D,S,/*unspecified*/>
MakeSignal(F&& func, const Signal<D,TValues>& ... args);
Creates a new signal node with value v = func(args.Value(), ...)
.
This value is set on construction and updated when any args
have changed.
template
<
typename D,
typename T
>
Signal<D,T> Flatten(const Signal<D,Signal<D,T>>& other);
TODO
namespace react
{
//
// Overloaded unary operators
// Arithmetic: + - ++ --
// Bitwise: ~
// Logical: !
//
// OP <Signal>
TempSignal<D,S,/*unspecified*/>
OP(const TSignal& arg)
// OP <TempSignal>
TempSignal<D,S,/*unspecified*/>
OP(TempSignal<D,TVal,/*unspecified*/>&& arg);
//
// Overloaded binary operators
// Arithmetic: + - * / %
// Bitwise: & | ^
// Comparison: == != < <= > >=
// Logical: && ||
//
// NOT overloaded:
// Bitwise shift: << >>
//
// <Signal> BIN_OP <Signal>
TempSignal<D,S,/*unspecified*/>
BIN_OP(const TLeftSignal& lhs, const TRightSignal& rhs)
// <Signal> BIN_OP <NonSignal>
TempSignal<D,S,/*unspecified*/>
BIN_OP(const TLeftSignal& lhs, TRightVal&& rhs);
// <NonSignal> BIN_OP <Signal>
TempSignal<D,S,/*unspecified*/>
BIN_OP(TLeftVal&& lhs, const TRightSignal& rhs);
// <TempSignal> BIN_OP <TempSignal>
TempSignal<D,S,/*unspecified*/>
BIN_OP(TempSignal<D,TLeftVal,/*unspecified*/>&& lhs,
TempSignal<D,TRightVal,/*unspecified*/>&& rhs);
// <TempSignal> BIN_OP <Signal>
TempSignal<D,S,/*unspecified*/>
BIN_OP(TempSignal<D,TLeftVal,/*unspecified*/>&& lhs,
const TRightSignal& rhs);
// <Signal> BIN_OP <TempSignal>
TempSignal<D,S,/*unspecified*/>
BIN_OP(const TLeftSignal& lhs,
TempSignal<D,TRightVal,/*unspecified*/>&& rhs)
// <TempSignal> BIN_OP <NonSignal>
TempSignal<D,S,/*unspecified*/>
BIN_OP(TempSignal<D,TLeftVal,/*unspecified*/>&& lhs,
TRightVal&& rhs);
// <NonSignal> BIN_OP <TempSignal>
TempSignal<D,S,/*unspecified*/>
BIN_OP(TLeftVal&& lhs,
TempSignal<D,TRightVal,/*unspecified*/>&& rhs);
}