Skip to content
Sebastian Jeckel edited this page Jun 22, 2014 · 8 revisions

Motivation

The previously introduced signals handle data changes, 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.

Basics

For each of the following examples, 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, sequential)

Hello World

We start by creating an EventSource that can emit strings:

#include <string>
using std::string;

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

Analogously to signals, D::EventSourceT<E> and D::MakeEventSource<E>() are aliases for EventSource<D,E> and MakeEventSource<D,E>(). EventSource and Events are the respective counterparts of VarSignal and Signal.

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

// Or as a function call:
mySource(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.

The syntactic alternatives are there to allow to distingish between several use cases. For instance, if an event is used like a function triggering an action, use function syntax. If it acts more like a data stream, 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 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 = MakeEventSource<D>();
Observe(helloWorldTrigger, [] (Token) {
    std::cout << "Hello world" << std::endl;
});

helloWorldTrigger.Emit();

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

// Function-style:
helloWorldTrigger();

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  = MakeEventSource<D>();
D::EventSourceT<> rightClick = MakeEventSource<D>();

D::EventsT<>      anyClick   = Merge(leftClick, rightClick);
Observe(anyClick, [] (Token) {
    std::cout << "clicked" << std::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 | for merging:

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 both in the same turn. The order in which the merged events are received is not defined and can differ between compiler implementations.

Filtering events

Event streams can be filtered with Filter:

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

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

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

Transforming events

Events can be transformed with Transform. The following example tags a stream of numbers by wrapping them into a pair with their tag:

#inclue <utility>
using std::pair;

enum class ETag { normal, critical };

using TaggedNum = pair<ETag,int>;

D::EventSourceT<int>  numbers = MakeEventSource<D,int>();
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 = MakeEventSource<D,int>();

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

Details

TODO