Skip to content
Sebastian Jeckel edited this page May 25, 2014 · 8 revisions

Motivation

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.

Tutorials

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);

Hello World

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.

Merging event streams

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.

Filtering events

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

Transforming events

#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

Queuing multiple inputs

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

Details

TODO

Clone this wiki locally