-
Notifications
You must be signed in to change notification settings - Fork 130
Events
The previously introduced signals revolve around change propagation of persistent data, but those are not the only types events that exist in a program. Button presses or the resize of a window are not something that is meant to be stored; they should rather should trigger certain actions.
When implemented with basic callback registries, things get difficult as soon as actions can no longer be attributed to a single event cause, but happen under more complex conditions. This problem is resolved by introducing event streams as first class objects. As such, they can be composed, merged, filtered or transformed.
The underlying propagation engine is the same as used for signals, thus offering the same benefits of scalable updating and implicit parallelization.
For each of the following tutorials, these header files have to be included:
#include "react/Domain.h"
#include "react/Event.h"
#include "react/Observer.h"
A domain is defined as:
REACTIVE_DOMAIN(D);
We start by creating an EventSource
that can emit strings:
#include <string>
using std::string;
D::EventSourceT<string> mySource = D::MakeEventSource<string>();
Analogously to signals, D::EventSourceT<E>
and D::MakeEventSource<E>()
are aliases for EventSource<D,E>
and MakeEventSource<D,E>()
.
Next, we add an observer that prints strings emitted by mySource
:
Observe(mySource, [] (const string& s) {
std::cout << s << std::endl;
});
Lastly, we emit a string from the source:
mySource << string("Hello world");
// Or without the operator:
mySource.Emit(string("Hello world"));
Note that the opererator used here is <<
and not <<=
as used for signals.
This is to indicate that event streams don't hold values, they only propagate them, whereas signal input combines assignment and change propagation.
It's not uncommon that the value type transported by an event stream is irrelevant and we are only interested in the fact that it happened. For instance, when a button has been clicked. In this case, the value type can be omitted.
We create a second version of the "Hello world" program to demonstrate this:
D::EventSourceT<> helloWorldTrigger = D::MakeEventSource();
Observe(helloWorldTrigger, [] (Token) {
std::cout << "Hello world" << std::endl;
});
helloWorldTrigger.Emit();
Internally, the value transported by token streams is of type Token
, hence an unnamed argument of this type was used for the lambda.
Earlier versions of the API allowed to omit the event argument completely if it's a token, but for consistency reasons it always has to be declared now.
Events from multiple streams can be merged to a single stream:
D::EventSourceT<> leftClick = D::MakeEventSource();
D::EventSourceT<> rightClick = D::MakeEventSource();
D::EventsT<> anyClick = Merge(leftClick, rightClick);
Observe(anyClick, [] (Token) {
std::cout << "clicked" << std::endl;
});
leftClick.Emit(); // output: clicked
rightClick.Emit(); // output: clicked
EventSource
is to Events
what VarSignal
is to Signal
.
Merge
takes a variable number of arguments, so more than two streams can be merged at once.
An alternative is using the overloaded |
operator for event streams:
D::EventsT<> anyClick = leftClick | rightClick;
It should be noted that if multiple source streams emit in the same turn, the merged stream will receive all of them.
For the previous example this means if leftClick
and rightClick
emit a token simultaneously, anyClick
receives two tokens.
The order in which the merged events are received is not defined and can differ between compiler implementations.
D::EventSourceT<int> numbers = D::MakeEventSource();
D::EventsT<int> greater10 = Filter(numbers, [] (int n) {
return n > 10;
});
Observe(greater10, [] (int n) {
std::cout << n << std::endl;
});
numbers << 5 << 11 << 7 << 100; // output: 11, 100
#inclue <utility>
using std::pair;
enum class ETag { normal, critical };
using TaggedNum = pair<ETag,int>;
D::EventSourceT<int> numbers = D::MakeEventSource();
D::EventsT<TaggedNum> tagged = Transform(numbers, [] (int n) {
if (n > 10)
return TaggedNum{ ETag::critical, n };
else
return TaggedNum{ ETag::normal, n };
});
Observe(tagged, [] (const TaggedNum& t) {
if (t.first == ETag::critical)
std::cout << "(critical) " << t.second << std::endl;
else
std::cout << "(normal) " << t.second << std::endl;
});
numbers << 5; // output: (normal) 5
numbers << 20; // output: (critical) 20
Queing multiple inputs in a single turn works analogously to signals:
D::DoTransaction([] {
src << 1 << 2 << 3;
src << 4;
});
It's possible to mix signal and event input in the same transaction.
Unlike signals, where only the last value change for each signal is used, event streams will forward all queued values:
D::EventSourceT<int> src = D::MakeEventSource<int>();
Observe(src, [] (int v) {
std::cout << v << std::endl;
}); // output: 1, 2, 3, 4
TODO