Skip to content

Commit 1826650

Browse files
committed
Tweaked BasicSignals, added BasicEvents tutorial.
1 parent 35bb608 commit 1826650

2 files changed

Lines changed: 219 additions & 21 deletions

File tree

tutorials/BasicEvents.md

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
---
2+
layout: default
3+
title: Event basics
4+
groups:
5+
- {name: Home, url: ''}
6+
- {name: Tutorials , url: 'tutorials/'}
7+
---
8+
9+
- [Preface](#preface)
10+
- [Hello world](#hello-world)
11+
- [Merging event streams](#merging-event-streams)
12+
- [Processing events](#filtering-events)
13+
- [Changing multiple inputs](#changing-multiple-inputs)
14+
15+
## Preface
16+
17+
This tutorial covers the event basics.
18+
It's assumed that the previous tutorial on signals has been understood.
19+
20+
21+
## Hello World
22+
23+
We start by creating an `EventSource` that can emit strings:
24+
{% highlight C++ %}
25+
#include "react/Domain.h"
26+
#include "react/Event.h"
27+
28+
REACTIVE_DOMAIN(D, sequential)
29+
USING_REACTIVE_DOMAIN(D)
30+
31+
EventSourceT<string> mySource = MakeEventSource<D,string>();
32+
{% endhighlight %}
33+
`EventSource` and `Events` are the respective counterparts of `VarSignal` and `Signal`.
34+
35+
Analogously to VarSignalT<S>, `EventSourceT<E>` is an alias for `EventSource<D,E>` defined by `USING_REACTIVE_DOMAIN`.
36+
37+
Unlike signals, event streams are purely push-based.
38+
They forward values to be processed, but don't hold on to them.
39+
There is no equivalent to the `Value()` accessor of `Signals`.
40+
41+
This means to do anything useful with `mySource`, we have to add an observer:
42+
43+
{% highlight C++ %}
44+
Observe(mySource, [] (string s) {
45+
cout << s << endl;
46+
});
47+
{% endhighlight %}
48+
{% highlight C++ %}
49+
mySource.Emit(string( "Hello world" ));
50+
51+
// ... or with operator
52+
mySource << string( "Hello world" );
53+
{% endhighlight %}
54+
55+
Note that here we use the conventional stream operator `<<`, rather than `<<=`, which is used for signals.
56+
The reasoning behind this is that event input is propagation-only, whereas signal input is both assignment and propagation.
57+
In other words: Signals hold a value, event streams don't. The different operators are meant to symbolize that.
58+
59+
There's a third syntactic alternative that treats `mySource` as a function object:
60+
{% highlight C++ %}
61+
mySource(string( "Hello world" ));
62+
{% endhighlight %}
63+
64+
These syntactic alternatives allow to distingish between several use cases.
65+
For instance, if an event is used like a function triggering an action, function-style is appropriate.
66+
If it acts more like a data stream, we can use the stream syntax.
67+
68+
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.
69+
For instance, when a button has been clicked. In this case, the value type can be omitted.
70+
We create a second version of the "Hello world" program to demonstrate this:
71+
{% highlight C++ %}
72+
D::EventSourceT<> helloWorldTrigger = MakeEventSource<D>();
73+
{% endhighlight %}
74+
{% highlight C++ %}
75+
Observe(helloWorldTrigger, [] (Token) {
76+
cout << "Hello world" << endl;
77+
});
78+
{% endhighlight %}
79+
{% highlight C++ %}
80+
helloWorldTrigger.Emit();
81+
82+
// Stream-style:
83+
helloWorldTrigger << Token::value;
84+
85+
// Function-style:
86+
helloWorldTrigger();
87+
{% endhighlight %}
88+
89+
Internally, the value transported by token streams is of type `Token`, hence for the observer function, we added an unnamed argument of this type.
90+
91+
92+
## Merging event streams
93+
94+
From what we've seen so far, event streams are little more than callback registries;
95+
but their true strength is composability.
96+
97+
For example, lets say we have two event sources that represent different mouse buttons.
98+
We can easily merge them to a single stream that contains events from both buttons:
99+
{% highlight C++ %}
100+
EventSourceT<> leftClick = MakeEventSource<D>();
101+
EventSourceT<> rightClick = MakeEventSource<D>();
102+
103+
EventsT<> anyClick = Merge(leftClick, rightClick);
104+
{% endhighlight %}
105+
{% highlight C++ %}
106+
Observe(anyClick, [] (Token) {
107+
cout << "button clicked" << endl;
108+
});
109+
{% endhighlight %}
110+
{% highlight C++ %}
111+
leftClick.Emit(); // output: clicked
112+
rightClick.Emit(); // output: clicked
113+
{% endhighlight %}
114+
`Merge` takes a variable number of arguments, so more than two streams can be merged at once.
115+
116+
An alternative is using the overloaded `|` for merging:
117+
{% highlight C++ %}
118+
EventsT<> anyClick = leftClick | rightClick;
119+
{% endhighlight %}
120+
121+
122+
## Processing events
123+
124+
Besides merging events from multipe streams, we can also process the events themselves.
125+
126+
First, let's demonstrate this by filtering a stream of numbers:
127+
{% highlight C++ %}
128+
EventSourceT<int> numbers = MakeEventSource<D,int>();
129+
130+
EventsT<int> greater10 = Filter(numbers, [] (int n) {
131+
return n > 10;
132+
});
133+
{% endhighlight %}
134+
{% highlight C++ %}
135+
Observe(greater10, [] (int n) {
136+
cout << n << endl;
137+
});
138+
{% endhighlight %}
139+
{% highlight C++ %}
140+
numbers << 5 << 11 << 7 << 100; // output: 11, 100
141+
{% endhighlight %}
142+
If the filter predicate function returns true for the passed value, it will be forwarded. Otherwise, it's filtered out.
143+
144+
Events can be transformed with `Transform`.
145+
146+
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:
147+
{% highlight C++ %}
148+
enum Tag { normal, critical };
149+
150+
using TaggedNum = pair<Tag,int>;
151+
152+
EventSourceT<int> numbers = MakeEventSource<D,int>();
153+
EventsT<TaggedNum> tagged = Transform(numbers, [] (int n) {
154+
if (n > 10)
155+
return TaggedNum( critical, n );
156+
else
157+
return TaggedNum( normal, n );
158+
});
159+
{% endhighlight %}
160+
{% highlight C++ %}
161+
Observe(tagged, [] (const TaggedNum& t) {
162+
if (t.first == critical)
163+
cout << "(critical) " << t.second << std::endl;
164+
else
165+
cout << "(normal) " << t.second << std::endl;
166+
});
167+
{% endhighlight %}
168+
{% highlight C++ %}
169+
numbers << 5;
170+
// output: (normal) 5
171+
172+
numbers << 20;
173+
// output: (critical) 20
174+
{% endhighlight %}
175+
176+
177+
## Changing multiple inputs
178+
179+
Queing multiple inputs in a single turn works analogously to signals:
180+
{% highlight C++ %}
181+
DoTransaction<D>([] {
182+
src << 1 << 2 << 3;
183+
src << 4;
184+
});
185+
{% endhighlight %}
186+
It's possible to mix signal and event input in the same transaction.
187+
188+
Unlike signals, where only the last value change for each signal is used, event streams will forward all queued values:
189+
{% highlight C++ %}
190+
EventSourceT<int> src = MakeEventSource<D,int>();
191+
192+
Observe(src, [] (int v) {
193+
cout << v << endl;
194+
});
195+
// Turn #1, output: 1, 2, 3, 4
196+
{% endhighlight %}

tutorials/BasicSignals.md

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,22 @@ groups:
1414
- [Modifying inputs in-place](#modifying-inputs-in-place)
1515

1616
## Preface
17+
1718
This tutorial demonstrates basic usage of signals.
1819

1920
The following code examples are kept fairly brief to focus on the presented features.
2021
Repeated definitions are omitted and we assume that the used types and functions from the standard library, i.e. `std::string` or `std::cout`, are available in the current namespace.
2122
Working source code to accompany this tutorial can be found [here]().
2223

24+
2325
## Defining a domain
2426

25-
Reactive values are grouped by logical domains.
27+
Each reactive value belongs to a logical domains. Their purpose is
28+
29+
* grouping related reactive values together and encapsulating them;
30+
* allowing different concurrency policies for different domains;
31+
* simplifying management of dependency relations by splitting up the dependency graph into smaller pieces.
32+
2633
Hence, the first thing we do is defining a domain:
2734

2835
{% highlight C++ %}
@@ -34,25 +41,20 @@ REACTIVE_DOMAIN(MyDomain, sequential)
3441
{% endhighlight %}
3542

3643
The first parameter of the `REACTIVE_DOMAIN` macro is the domain name.
37-
Technically, a domain is a type, so it can be aliased with `using`/`typedef` or used as a type parameter for templates.
44+
Technically, a domain is a type, so it can be aliased or used as a type parameter for templates.
3845
Think of it as being declared as `class MyDomain : /*...*/`.
3946

4047
The second parameter specifies the concurrency policy, which controls
4148

42-
* (1) concurrent input and
43-
* (2) parallel updating.
44-
45-
For this tutorial, neither is needed, so we use `sequential`.
49+
* concurrent input, and
50+
* parallel updating.
4651

47-
The purpose of domains is
52+
For this tutorial, neither is relevant, so we use `sequential`.
4853

49-
* grouping related reactive values together and encapsulating them;
50-
* allowing different concurrency policies for different domains;
51-
* simplifying management of dependency relations by splitting up the respective graph into smaller pieces.
5254

5355
## Hello world
5456

55-
Next, we are going to create a simple signal that holds the string `Hello world`:
57+
Next, we create a simple signal that holds the string `"Hello world"`:
5658
{% highlight C++ %}
5759
#include "react/Domain.h"
5860
#include "react/Signal.h"
@@ -64,15 +66,15 @@ REACTIVE_DOMAIN(D, sequential)
6466
VarSignal<D,string> myString = MakeVar<D>(string( "Hello world" ));
6567
{% endhighlight %}
6668

67-
`D` was used as the domain name for its shortness, as it will be for the remainder of this tutorial.
69+
`D` is used as the domain name for its shortness, as it will be for the remainder of this tutorial.
6870

6971
Conceptionally, an instance of type `Signal<D,S>` is a reactive container that holds a single value of type `S` and belongs to domain `D`.
70-
`myString` was declared as a `VarSignal`, which allows you to modify its value later; declaring it as `Signal` would've made it read-only.
72+
`myString` is declared as a `VarSignal`, which allows us to modify its value later; declaring it as `Signal` would've made it read-only.
7173

72-
The `MakeVar<D>` function takes a value of type `S` and returns a new `VarSignal<D,S>`, which initially holds that value.
74+
The `MakeVar<D>` function takes a value of type `S` and returns a new `VarSignal<D,S>`, which initially holds the given value.
7375

74-
`myString` is similar to an "ordinary" variable; its value can be read imperatively with `myString.Value()` and changed with `myString.Set(x)`.
75-
An alternative to `Set(x)` is the overloaded `<<=` operator, i.e. `myString <<= x`.
76+
`myString` is similar to an ordinary variable; its value can be read imperatively with `Value()` and changed with `Set(x)`.
77+
An alternative to `myString.Set(x)` is the overloaded `<<=` operator, i.e. `myString <<= x`.
7678
This should not be mixed up with the assignment operator, which is reserved to assign the signal itself, not the inner value.
7779

7880
So far that's not very reactive - let's make it more interesting.
@@ -101,9 +103,9 @@ SignalT<string> bothWords =
101103
The macro `USING_REACTIVE_DOMAIN(name)` defines aliases for reactive types of the given domain in the current scope.
102104
This allows us to use `VarSignalT<S>` instead of `VarSignal<D,S>`.
103105

104-
`firstWord` and `secondWord` are two `VarSignals` we can change later.
106+
`firstWord` and `secondWord` are two `VarSignals` that can be changed later.
105107

106-
With `MakeSignal`, we have connected the values of the signals in the `With(...)` expression to the function arguments of `concatFunc`.
108+
`MakeSignal` connects the values of the signals in the `With(...)` expression to the function arguments of `concatFunc`.
107109
The value type of the created signal matches the return type of the function.
108110
The value of `bothWords` is automatically set by calling `concatFunc(firstWord.Value(), secondWord.Value())`.
109111
This happens to set the initial value and when `firstWord` or `secondWord` have been changed.
@@ -118,7 +120,7 @@ secondWord <<= string( "world" );
118120
cout << bothWords.Value() << endl; // output: "Hello world"
119121
{% endhighlight %}
120122

121-
`MakeSignal` accepts any type of function, including lambdas:
123+
`MakeSignal` accepts any type of function, including lambdas or `std::bind` expressions:
122124
{% highlight C++ %}
123125
SignalT<string> bothWords =
124126
MakeSignal(
@@ -149,11 +151,11 @@ Either the value of a signal is a function of its dependent signals, or its valu
149151

150152
## Reacting to value changes
151153

152-
In the previous example, we first pushed new values with `<<=`, then pulled a result with `Value()`.
154+
In the previous example, new values were pushed with `<<=`, then the result was pulled `Value()`.
153155
There are some issues with this approach:
154156

155157
* Concurrent pushes and pulls are not thread-safe, so they have to be coordinated somehow;
156-
* `Value()` is not suitable if we want to react to value changes.
158+
* `Value()` is not suitable to react to value changes.
157159

158160
There can be situations where the use of `Value()` is appropriate and we used it in the initial examples to demonstrate the basic idea behind signals.
159161
However, in most cases a push-based approach should be preferred.

0 commit comments

Comments
 (0)