HomeTutorials ⇀ Event streams

Event streams

Hello World

The previously shown signals hold state and push changes to their dependents. Event streams, on the other hand, are used to model more generic event types that are not coupled to state, for example mouse clicks.

We start the tutorial by creating an EventSource that can emit strings:

#include "react/Domain.h"
#include "react/Event.h"

REACTIVE_DOMAIN(D, sequential)
USING_REACTIVE_DOMAIN(D)

EventSourceT<string> mySource = MakeEventSource<D,string>();

EventSource and Events are the respective counterparts of VarSignal and Signal.

Analogously to VarSignalT<S>, EventSourceT<E> is an alias for EventSource<D,E> defined by USING_REACTIVE_DOMAIN.

Unlike signals, event streams are purely push-based. They forward values to be processed, but don’t hold on to them. There is no equivalent to the Value() accessor of Signal.

This means to do anything useful with mySource, we have to add an observer:

Observe(mySource, [] (string s) {
    cout << s << endl;
});
mySource.Emit(string( "Hello world" ));

// ... or with operator
mySource << string( "Hello world" );

Note that here we use the conventional stream operator <<, rather than <<=, which is used for signals. The reasoning behind this is that event input is propagation-only, whereas signal input is both assignment and propagation. In other words: Signals hold a value, event streams don’t. The different operators are meant to symbolize that.

There’s a third syntactic alternative that treats mySource as a function object:

mySource(string( "Hello world" ));

These syntactic alternatives allow to distingish between several use cases. For instance, if an event is used like a function triggering an action, function-style is appropriate. If it acts more like a data stream, we can use the stream syntax.

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 occurred. 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:

EventSourceT<> helloWorldTrigger = MakeEventSource<D>();
Observe(helloWorldTrigger, [] (Token) {
    cout << "Hello world" << endl;
});
helloWorldTrigger.Emit();

// Stream-style:
helloWorldTrigger << Token::value;

// Function-style:
helloWorldTrigger();

Internally, the value transported by token streams is of type Token, hence for the observer function, we added an unnamed argument of this type.

Merging event streams

From what we’ve seen so far, event streams are little more than callback registries; but their true strength is composability.

For example, lets say we have two event sources that represent different mouse buttons. We can easily merge them to a single stream that contains events from both buttons:

EventSourceT<> leftClick  = MakeEventSource<D>();
EventSourceT<> rightClick = MakeEventSource<D>();

EventsT<>      anyClick   = Merge(leftClick, rightClick);
Observe(anyClick, [] (Token) {
    cout << "button clicked" << endl;
});
leftClick.Emit();  // output: clicked
rightClick.Emit(); // output: clicked

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 merging:

EventsT<> anyClick = leftClick | rightClick;

Processing events

Besides merging events from multipe streams, we can also process the events themselves.

First, let’s demonstrate this by filtering a stream of numbers:

EventSourceT<int> numbers = MakeEventSource<D,int>();

EventsT<int> greater10 = Filter(numbers, [] (int n) {
    return n > 10;
});
Observe(greater10, [] (int n) {
    cout << n << endl;
});
numbers << 5 << 11 << 7 << 100; // output: 11, 100

If the filter predicate function returns true for the passed value, it will be forwarded. Otherwise, it’s filtered out.

Events can also be transformed with Transform. In other functional programming languages this operation would be called map, but we stick to the naming of std::transform. For example, we can transform a stream of numbers into a std::pair of the number and a tag that indicates whether the former exceeded a certain threshold:

enum Tag { normal, critical };

using TaggedNum = pair<Tag,int>;

EventSourceT<int>  numbers = MakeEventSource<D,int>();
EventsT<TaggedNum> tagged  = Transform(numbers, [] (int n) {
    if (n > 10)
        return TaggedNum( critical, n );
    else
        return TaggedNum( normal, n );
});
Observe(tagged, [] (const TaggedNum& t) {
    if (t.first == critical)
        cout << "(critical) " << t.second << endl;
    else
        cout << "(normal)  " << t.second << endl;
});
numbers << 5;
// output: (normal) 5

numbers << 20; 
// output: (critical) 20

Changing multiple inputs

Queing multiple inputs in a single turn works analogously to signals:

DoTransaction<D>([] {
    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:

EventSourceT<int> src = MakeEventSource<D,int>();

Observe(src, [] (int v) {
    cout << v << endl;
});
// Turn #1, output: 1, 2, 3, 4