# mini-rx: Tiny ~~reactive programming~~ *change propagation* a la [scala.rx](https://github.com/lihaoyi/scala.rx) [Cargo documentation](https://docs.rs/mini-rx) ## Example ```rust use mini_rx::*; fn example() { // setup let side_effect = Cell::new(0); let side_effect2 = RefCell::new(String::new()); // The centralized data dependency graph let mut g = RxDAG::new(); // Create variables which you can set let var1 = g.new_var(1); let var2 = g.new_var("hello"); assert_eq!(var1.get(g.now()), &1); assert_eq!(var2.get(g.now()), &"hello"); var1.set(&g, 2); var2.set(&g, "world"); assert_eq!(var1.get(g.now()), &2); assert_eq!(var2.get(g.now()), &"world"); // Create computed values which depend on these variables... let crx1 = g.new_crx(move |g| var1.get(g) * 2); // ...and other Rxs let crx2 = g.new_crx(move |g| format!("{}-{}", var2.get(g), crx1.get(g) * 2)); // ...and create multiple values which are computed from a single function let (crx3, crx4) = g.new_crx2(move |g| var2.get(g).split_at(3)); assert_eq!(crx1.get(g.now()), &4); assert_eq!(crx2.get(g.now()), &"world-8"); assert_eq!(crx3.get(g.now()), &"wor"); assert_eq!(crx4.get(g.now()), &"ld"); var1.set(&g, 3); var2.set(&g, &"rust"); assert_eq!(crx1.get(g.now()), &6); assert_eq!(crx2.get(g.now()), &"rust-12"); assert_eq!(crx3.get(g.now()), &"rus"); assert_eq!(crx4.get(g.now()), &"t"); // Run side effects when a value is recomputed let var3 = g.new_var(Vec::from("abc")); let side_effect_ref = &side_effect; let side_effect_ref2 = &side_effect2; // borrowed values must outlive g but don't have to be static g.run_crx(move |g| { side_effect_ref.set(side_effect_ref.get() + var1.get(g)); side_effect_ref2.borrow_mut().push_str(&String::from_utf8_lossy(var3.get(g))); }); assert_eq!(side_effect.get(), 3); assert_eq!(&*side_effect2.borrow(), &"abc"); var1.set(&g, 4); g.recompute(); assert_eq!(side_effect.get(), 7); assert_eq!(&*side_effect2.borrow(), &"abcabc"); // Note that the dependencies aren't updated until .recompute or .now is called... var3.set(&g, Vec::from("xyz")); assert_eq!(side_effect.get(), 7); assert_eq!(&*side_effect2.borrow(), &"abcabc"); g.recompute(); assert_eq!(side_effect.get(), 11); assert_eq!(&*side_effect2.borrow(), &"abcabcxyz"); // the side-effect also doesn't trigger when none of its dependencies change var2.set(&g, "rust-lang"); g.recompute(); assert_eq!(side_effect.get(), 11); assert_eq!(&*side_effect2.borrow(), &"abcabcxyz"); assert_eq!(crx2.get(g.now()), &"rust-lang-16"); // lastly we can create derived values which will access or mutate part of the base value // which are useful to pass to children let dvar = var3.derive_using_clone(|x| &x[0], |x, char| { x[0] = char; }); assert_eq!(dvar.get(g.now()), &b'x'); dvar.set(&g, b'b'); assert_eq!(dvar.get(g.now()), &b'b'); assert_eq!(var3.get(g.now()), &b"byz"); dvar.set(&g, b'f'); assert_eq!(dvar.get(g.now()), &b'f'); assert_eq!(var3.get(g.now()), &b"fyz"); assert_eq!(&*side_effect2.borrow(), &"abcabcxyzbyzfyz"); } ``` ## Overview `mini-rx` is a bare-bones implementation of "reactive programming" in Rust with 1 dependency. It uses manual polling and integrates well with the borrow checker by storing all values in a central data dependency graph, `RxDAG`. The type of reactive programming is **signal-based**, which is similar to [scala.rx](https://github.com/lihaoyi/scala.rx) but different than most libraries (which are **stream-based**) and maybe not technically FRP. Instead of manipulating a stream of values, you manipulate variables which trigger other computed values to recompute, which in turn trigger other values to recompute and side effects to run. ## Key concepts/types - `RxDAG`: stores all your `Rx`s. Lifetime rules guarantee that they don't change while you have active references (see [Lifetimes](#lifetimes)) - `new_var`: creates a `Var` - `new_crx`: creates a `CRx` - `run_crx`: runs a side-effect, will re-run when any of the accessed `Var`s or `CRx`s change - `new_crx[n]`: creates `n` `CRx`s which come from a single computation - `recompute`: updates all `Var` and `CRx` values, but requires a mutable reference which ensures there are no active shared references to the old values - `now`: recomputes and then gets an `RxContext` so you can get values. It must recompute and thus requires a mutable reference. - `stale`: Does not recompute but will not return the most recently set values unless `recompute` was called. - `Var`: value with no dependencies which you explicitly set, and this triggers updates - `CRx`: value computed from dependencies - `RxContext`: allows you to read `Var` and `CRx` values. Accessible via `RxDAG::now` or in computations (`RxDAG::new_crx`) and side-effects (`RxDAG::run_crx`) - `MutRxContext`: allows you to write to `Var`s. An `&RxDAG` is a `MutRxContext`. You cannot set values in a `CRx` computation because they are inputs. ## Signal-based The type of reactive programming is **signal-based**, which is similar to [scala.rx](https://github.com/lihaoyi/scala.rx) but different than most libraries (which are **stream-based**). Instead of manipulating a stream of values, you manipulate variables which trigger other computed values to recompute, which in turn trigger other values to recompute and side effects to run. You can simulate stream-based reactivity by adding a side-effect which pushes values on trigger, like so: ```rust use mini_rx::*; fn stream_like() { let stream = RefCell::new(Vec::new()); let stream_ref = &stream; let input1 = vec![1, 2, 3]; let input2 = vec![0.5, 0.4, 0.8]; let mut g = RxDAG::new(); let var1 = g.new_var(0); let var2 = g.new_var(0.0); let crx = g.new_crx(move |g| *var1.get(g) as f64 + *var2.get(g)); g.run_crx(move |g| { stream_ref.borrow_mut().push(*crx.get(g)); }); assert_eq!(&*stream.borrow(), &vec![0.0]); for (a, b) in input1.iter().zip(input2.iter()) { var1.set(&g, *a); var2.set(&g, *b); g.recompute(); } assert_eq!(&*stream.borrow(), &vec![0.0, 1.5, 2.4, 3.8]); } ``` For more traditional stream-based reactive programming, I recommend [reactive-rs](https://docs.rs/reactive-rs/latest/reactive_rs/) ## Lifetimes You can't obtain a mutable reference to the value stored within a `Var`. Instead you call `Var::set` or `Var::modify` with a completely new value. This is because there may be active references to the old `Var`. When you call `Var::set` it doesn't immedidately change the old value, so those references won't change. In order to actually update the reactive values and run side-effects, you must call `RxDAG::recompute`, or a function which internally calls `recompute` like `RxDAG::now`. In order to do this, you need a mutable refernce to the `RxDAG`, which you can only get if there are no active references to any of the reactive values. Additionally, any compute function in the `RxDAG` must live longer than the `RxDAG` itself. This is because the function may be called any time while the `RxDAG` is alive, when it gets recomputed. So if you have values which you reference in `CRx` computations or side-effects, you must either declare them before the `RxDAG` or use something like a `Weak` reference to ensure that they are still alive when used. ## Why? Signal-based Reactive programming 101 Here's a situation commonly encountered in programming: you have a value `a` which should always equal `b + c`. You don't want `a` to be a function, but when `b` or `c` changes, `a` must be recalculated. Or here's another situation: you have an action which must always run when a value changes, which sends the updated value to the server. You can chain these. Perhaps the value you want to send to the server on update is `a`. Perhaps `b` and `c` are computed from other values, `d, e, f`, and so on. Ultimately, for the theoretical folks, you have a DAG (directed-acyclic-graph) of values and dependencies. Modify one of the roots, and it triggers a cascade of computations and side effects.