Expand description
The bidule FRP crate.
This crate provides few simple primitives to write FRP-driven programs. Everything revolves
around the concept of a Stream
.
§Streams
A Stream
is a stream of typed signals. A stream of signals will get a signal as input
and will broadcast it downwards. You can compose streams with each other with very simple
combinators, such as map
, filter
, filter_map
, zip
, unzip
, merge
, fold
, etc. (non-exhaustive list).
§Creating streams and send signals
Streams are typed. You can use type inference or give them an explicit type:
use bidule::Stream;
let my_stream: Stream<i32> = Stream::new();
That’s all you need to create a stream. A stream represent a value that will be flowing in at some time.
Even though it’s not strictly the same thing, you can see a similitude with futures.
When you’re ready to send signals, just call the send
function:
use bidule::Stream;
let my_stream: Stream<i32> = Stream::new();
my_stream.send(&1);
my_stream.send(&2);
my_stream.send(&3);
§Observing signals
A single stream like that one won’t do much – actually, it’ll do nothing. The first thing we
might want to do is to subscribe a closure to do something when a signal is emitted. This is
done with the Stream::observe
function.
use bidule::Stream;
let my_stream: Stream<i32> = Stream::new();
my_stream.observe(|sig| {
// print the signal on stdout each time it’s flowing in
println!("signal: {:?}", sig);
});
my_stream.send(&1);
my_stream.send(&2);
my_stream.send(&3);
§FRP basics
However, FRP is not about callbacks. It’s actually the opposite, to be honest. We try to reduce
the use of callbacks as much as possible. FRP solves this by inversing the way you must work:
instead of subscribing callbacks to react to something, you transform that something to create
new values or objects. This is akin to the kind of transformations you do with Future
.
Let’s get our feet wet: let’s create a new stream that will only emit signals for even values:
use bidule::Stream;
let int_stream: Stream<i32> = Stream::new();
let even_stream = int_stream.filter(|x| x % 2 == 0);
even_stream
has type Stream<i32>
and will only emit signals when the input signal is even
.
Let’s try something more complicated: on those signals, if the value is less or equal to 10,
output "Hello, world!"
; otherwise, output "See you!"
.
use bidule::Stream;
let int_stream: Stream<i32> = Stream::new();
let even_stream = int_stream.filter(|x| x % 2 == 0);
let str_stream = even_stream.map(|x| if *x <= 10 { "Hello, world!" } else { "See you!" });
This is really easy; no trap involved.
Ok, let’s try something else. Some kind of a Hello world for FRP.
use bidule::Stream;
enum Button {
Pressed,
Released
}
fn unbuttonify(button: &Button, v: i32) -> Option<i32> {
match *button {
Button::Released => Some(v),
_ => None
}
}
let minus = Stream::new();
let plus = Stream::new();
let counter =
minus.filter_map(|b| unbuttonify(b, -1))
.merge(&plus.filter_map(|b| unbuttonify(b, 1)))
.fold(0, |a, x| a + x);
In this snippet, we have two buttons: minus
and plus
. If we hit the minus
button, we want
a counter to be decremented and if we hit the plus
button, the counter must increment.
FRP solves that problem by expressing counter
in terms of both minus
and plus
. The first
thing we do is to map a number on the stream that broadcasts button signals. Whenever that
signal is a Button::Released
, we return a given number. For minus
, we return -1
and for
plus
, we return 1
– or +1
, it’s the same thing. That gives us two new streams. Let’s see
the types to have a deeper understanding:
minus: Stream<Button>
plus: Stream<Button>
minus.filter_map(|b| unbuttonify(b, -1)): Stream<i32>
plus.filter_map(|b| unbuttonify(b, 1)): Stream<i32>
The merge
method is very simple: it takes two streams that emit the same type of signals and
merges them into a single stream that will broadcasts both the signals:
minus.filter_map(|b| unbuttonify(b, -1)).merge(plus.filter_map(|b| unbuttonify(b, 1)): Stream<i32>): Stream<i32>
The next and final step is to fold
those i32
into the final value of the counter by applying
successive additions. This is done with the fold
method, that takes the initial value – in the
case of a counter, it’s 0
– and the function to accumulate, with the accumulator as first
argument and the iterated value as second argument.
The resulting stream, which type is Stream<i32>
, will then contain the value of the counter.
You can test it by sending Button
signals on both minus
and plus
: the resulting signal
in counter
will be correctly decrementing or incrementing.
There exist several more, interesting combinators to work with your streams. For instance, if
you don’t want to map a function over two streams to make them compatible with each other – they
have different types, you can still perform some kind of a merge. That operation is called a
zip
and the resulting stream will yield either the value from the first – left – stream or the
value of the other – right – as soon as a signal is emitted. Its dual method is called unzip
and will split a stream apart into two streams if it’s a zipped stream. See Either
for further
details.
§Sinking
Sinking is the action to consume the signals of a stream and collect them. The current implementation uses non-blocking buffering when sending signals, and reading is up to you: the stream will collect the output signals in a buffer you can read via the Iterator trait. For instance, non-blocking reads:
use bidule::Stream;
enum Button {
Pressed,
Released
}
fn unbuttonify(button: &Button, v: i32) -> Option<i32> {
match *button {
Button::Released => Some(v),
_ => None
}
}
let minus = Stream::new();
let plus = Stream::new();
let counter =
minus.filter_map(|b| unbuttonify(b, -1))
.merge(&plus.filter_map(|b| unbuttonify(b, 1)))
.fold(0, |a, x| a + x);
let rx = counter.recv();
// do something with minus and plus
// …
for v in rx.try_iter() {
println!("read a new value of the counter: {}", v);
}
Structs§
- Stream
- A stream of signals.
Enums§
- Either
- Either one or another type.