2 unstable releases
0.2.0 | Jul 5, 2020 |
---|---|
0.1.0 | Jan 23, 2020 |
#953 in Rust patterns
82KB
1.5K
SLoC
Reax
Reax is a reactivity system for Rust that infers dependencies between functions.
Every Variable
managed by reax is a node in a dependency graph. Changes to and
usages of variables are tracked by the global ThreadRuntime
which updates the
graph to reflect actual variable accesses. There are two kinds of built-in
variables:
Var
which can be explicitly mutated.ComputedVar
which lazily computes its value with a function.
Users can listen to and react to changes of variables using .watch
.
Critically, a ComputedVar
will only re-compute when needed. If, when
computing its value, a ComputedVar
uses any other variable anywhere (directly
or indirectly), changes to any of those upstream variables will automatically
mark the computed variable as dirty and the variable's value will be recomputed
the next time it is used.
Examples
Reax builds a model of how the variables in your program interact as it runs.
use reax::prelude::*;
// Create input variables.
let number = Var::new(1).with_label("number");
let name = Var::new("Sam").with_label("name");
// Create computed variables.
let formatted = (&number)
.map(|x| format!("{}", x))
.with_label("formatted");
let printout = computed! {
output! text = String::new(); // Reuse a buffer
text.clear();
*text += *name.get();
*text += " sees ";
*text += formatted.get().as_str();
}.with_label("printout");
// The computed variables haven't been used yet. Nothing is hooked-up.
assert_eq!(printout.node().depends_on(formatted.node()), false);
// Use the variables!
assert_eq!(printout.get().as_str(), "Sam sees 1");
number.set(42);
name.set("Reax");
assert_eq!(printout.get().as_str(), "Reax sees 42");
// Reax now knows how data moves through the variables!
assert_eq!(printout.node().depends_on(formatted.node()), true);
// Print a .dot visualization.
reax::ThreadRuntime::write_graphviz(std::io::stdout().lock()).unwrap();
We can see this example through reax's eyes:
Reax will only update computed variables when needed.
use reax::prelude::*;
let number = Var::new(0);
let bigger_number = (&number).map(|x| *x + 10);
let even_bigger_number = (&bigger_number).map(|x| *x + 100);
let times_called = Var::new(0);
// Set up a watcher to track how often bigger_number changes.
let mut eval = EagerCompute::new(());
eval.watch(&bigger_number, |_| {
*times_called.mutate() += 1;
});
// The watcher is called once on creation.
assert_eq!(*times_called.get(), 1);
// Run the watcher. This is effectively a no-op since nothing has changed.
for _ in 0..100 { eval.tick(); }
// Update a variable.
number.set(1);
// Dependent variables are instantly dirty.
assert_eq!(bigger_number.node().is_dirty(), true);
assert_eq!(even_bigger_number.node().is_dirty(), true);
// Run the watcher again. This time it fires.
eval.tick();
assert_eq!(*times_called.get(), 2);
// even_bigger_number is still dirty since no one has used it yet.
assert_eq!(even_bigger_number.node().is_dirty(), true);
Here you can see how the variables downstream from number
are all
instantly marked when it changes. But they won't be recomputed until used:
Reax has no built-in understanding of collections so you can use nested
Var
s to better control the "depth" of changes.
use reax::prelude::*;
// Create a list of variables.
let list = Var::new(Vec::new());
for x in 1..=3 {
list.mutate().push(Var::new(x));
}
// Some computed properties:
let length = computed! { list.get().len() };
let sum = computed! {
list.get().iter().map(|elem| *elem.get()).sum::<i32>()
};
// Make length and sum outdated by pushing an extra element.
list.mutate().push(Var::new(4));
// Update the length.
length.check(&mut ());
// Now only make sum outdated, and leave it that way.
list.get()[0].set(100);
Visualizing the runtime at the end of this example, you can see that only the sum is dirty. None of the list elements are dependencies of the list itself so any changes to them don't effect variables that never read them. And reax hasn't seen that the extra element will be used in the sum. It will find that out the next time the sum is computed.