I'm looking for a crate that will allow me to create some variables and also some calculatable values that will depend on these variables and other calculatable values. I'm working on a UI system, the port of the existing system, and in the existing system we have such a thing. It has an interface like this:
// pseudocode
Value<T> {
fn get(): T; // get value
fn set(v: T): Void; // set constant value
fn set_compute(f: Fn()->T); // make this value computed
fn subscribe(f: Fn()); // get notification when value changes
}
// example
let a = Value::new(10);
let b = Value::new(20);
let c = Value::new();
c.set_compute(|| a.get()+b.get())
assert(c.get() == 30);
b.set(30);
assert(c.get() == 40);
If set_compute closure access some other Value and that value changes current value invalidates and will validate itself on the next call to get(). This is achieved by storing value calling set_compute in a global variable and on each call to get() this value is subscribed to the value for which get() was called.
I do not think it will be easy to recreate something like this in Rust from scratch. Mostly to my limited experience with Rust. But I expect a lot of problems with borrowing rules in such a system.
There are such crates. But I'm not sure I'm using the right keywords to find them, and also I do not know how to choose one.
futures-signals - one of the most popular. I tried to use it and failed. How do I get the value from signal? I tried using it with tokio and calling signal.to_future().await, but that just never resolved. Also, I do not think I need asynchronous code here. Maybe it is possible to use this in a synchronous manner.
leptos-reactive - much less popular. From what I read in the docs it's what I need. But their example is panicking and the error message is about some leptos stuff, I'm not interested in, like view! macro etc.
mini-rx - not popular at all, but does what it claims, their example just compiled and run as expected. But I see that it may be not easy to use due to lifetimes requirements.
And actually that's what I've found, not including crates with near-to-zero downloads.
I have no experience of other crates you mentioned, but the futures-signals is not very far from your example pseudo code API. I assume you are familiar with rust async, e.g. you know how to use crates like futures, tokio etc (or alternatives like futures-lite, smol etc).
the usage is simple, you build a dependency graph of computations: use futures_signals::Mutable<T> to model the "input" node, use plain futures_core::Stream<Item = T> to model the "reactive" nodes. you need to explicitly call Mutable::signal() (or variants of it, see documentation) to subscribe to value change events.
the example pseudo code can be written as following:
// in async context
let a = Mutable::new(10);
let b = Mutable::new(20);
let c = map_ref! {
let a = a.signal(),
let b = b.signal() =>
*a + *b
};
let mut c = c.to_stream();
assert!(c.next().await.unwrap(), 30);
b.set(30);
assert!(c.next().await.unwrap(), 40);
please note, when you get a Stream from a Signal using SignalExt::to_stream(), the stream will not cache the previous value, and will resolve next item when polled only if at least one of the input signals changed (it will always resolve the first item).
if you need a cached value when no updates are propagated, you can cache it yourself (but you need a wrapper type to intercept the Stream::poll_next() method), or you can use the SignalExt::sample_stream_cloned() with an "sampler" (e.g. futures::stream::repeat(()) will unconditionally resolve whenever you poll_next() on it).
do be careful when you use "sampled" mode, you control the sample rate via the sampler now, you can no longer rely on the "input" nodes to drive the entire computation graph.
Being notified of changes is core to what most people consider "reactive", I think. Is that not something you need? In that case, just sharing a Cell<T> / RefCell<T> (probably wrapped in Rc if you need shared ownership) or Mutex<T> / RwLock<T> (wrapped in Arc if you need shared ownership) with a closure that you then invoke to get the latest value might be sufficient?
use std::cell::Cell;
fn main() {
let a = Cell::new(10);
let b = Cell::new(20);
let c = || a.get() + b.get();
assert_eq!(c(), 30);
b.set(30);
assert_eq!(c(), 40);
}
Being notified of changes is core to what most people consider "reactive", I think. Is that not something you need? In that case, just sharing a Cell<T> / RefCell<T> (probably wrapped in Rc if you need shared ownership) or Mutex<T> / RwLock<T> (wrapped in Arc if you need shared ownership) with a closure that you then invoke to get the latest value might be sufficient?
Unfortunately, it is not sufficient. I also need the ability to run side effects when the value changes. In my case, the window has to be redrawn when one of the parameters changes. I did not show this in an original post only because of short thinking.
Can you elaborate a little? I stumbled upon it myself, but I did not find any introduction or even short example, and decided that I will not try to read their code, just because that will take too much time for me.