bare-bones "reactive programming" (change propogation) using a central data dependency graph

Overview

mini-rx: Tiny reactive programming change propagation a la scala.rx

Cargo documentation

Example

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 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 Rxs. Lifetime rules guarantee that they don't change while you have active references (see 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 Vars or CRxs change
    • new_crx[n]: creates n CRxs 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 Vars. 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 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:

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

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.

You might also like...
A simple and convenient way to bundle owned data with a borrowing type.

A simple and convenient way to bundle owned data with a borrowing type. The Problem One of the main selling points of Rust is its borrow checker, whic

A library for transcoding between bytes in Astro Notation Format and Native Rust data types.

Rust Astro Notation A library for transcoding between hexadecimal strings in Astro Notation Format and Native Rust data types. Usage In your Cargo.tom

An implementation of a predicative polymorphic language with bidirectional type inference and algebraic data types

Vinilla Lang Vanilla is a pure functional programming language based on System F, a classic but powerful type system. Merits Simple as it is, Vanilla

A parser for the perf.data format

linux-perf-data This repo contains a parser for the perf.data format which is output by the Linux perf tool. It also contains a main.rs which acts sim

A HashMap/Vector hybrid: efficient, ordered key-value data storage in Rust.

hashvec A HashVec is a hash map / dictionary whose key-value pairs are stored (and can be iterated over) in a fixed order, by default the order in whi

Library and proc macro to analyze memory usage of data structures in rust.
Library and proc macro to analyze memory usage of data structures in rust.

Allocative: memory profiler for Rust This crate implements a lightweight memory profiler which allows object traversal and memory size introspection.

A compact generational arena data structure for Rust.

Compact Generational Arena This crate provides ArenaT, a contiguous growable container which assigns and returns IDs to values when they are added t

A real-time data backend for browser-based applications.

DriftDB DriftDB is a real-time data backend for browser-based applications. For more information, see driftdb.com. Structure of this repo docs/: main

A fast rendezvous in rust where data can optionally be swapped between the two threads.

rendezvous_swap A rendezvous is an execution barrier between a pair of threads, but this crate also provides the option of swapping data at the synchr

Owner
Jakob Hain
Purdue graduate student and Northeastern alumni
Jakob Hain
Comprehensive DSP graph and synthesis library for developing a modular synthesizer in Rust, such as HexoSynth.

HexoDSP - Comprehensive DSP graph and synthesis library for developing a modular synthesizer in Rust, such as HexoSynth. This project contains the com

Weird Constructor 45 Dec 17, 2022
🔀 Rusty flow graph processing library

flowing flowing is a flow graph processing library written in Rust. It shall serve as a general-purpose building block for all kinds of dataflow progr

Micha Hanselmann 2 Oct 4, 2022
An expression based data notation, aimed at transpiling itself to any cascaded data notation.

Lala An expression oriented data notation, aimed at transpiling itself to any cascaded data notation. Lala is separated into three components: Nana, L

null 37 Mar 9, 2022
Parse and encoding of data using the SCTE-35 standard.

SCTE-35 lib and parser for Rust Work in progress! This library provide access to parse and encoding of data using the SCTE-35 standard. This standard

Rafael Carício 4 May 6, 2022
Rust library for concurrent data access, using memory-mapped files, zero-copy deserialization, and wait-free synchronization.

mmap-sync mmap-sync is a Rust crate designed to manage high-performance, concurrent data access between a single writer process and multiple reader pr

Cloudflare 97 Jun 26, 2023
Rust Vector for large amounts of data, that does not copy when growing, by using full `mmap`'d pages.

Large Vector Rust Vector for large amounts of data, that does not copy when growing, by using full mmap'd pages. Maturity I made ths to learn about mm

Wonko der Verständige 21 Apr 23, 2024
Proof-of-concept for a memory-efficient data structure for zooming billion-event traces

Proof-of-concept for a gigabyte-scale trace viewer This repo includes: A memory-efficient representation for event traces An unusually simple and memo

Tristan Hume 59 Sep 5, 2022
Data structures and algorithms for 3D geometric modeling.

geom3d Data structures and algorithms for 3D geometric modeling. Features: Bezier curve and surface B-Spline curve and surface Spin surface Sweep surf

Junfeng Liu 31 Sep 20, 2022
Prometheus exporter that scrapes data in different formats

data-exporter A prometheus exporter that scrapes remote data or local files and converts them to prometheus metrics. It is similar to json_exporter, b

Fredrik Enestad 5 Sep 27, 2022
A tool to deserialize data from an input encoding, transform it and serialize it back into an output encoding.

dts A simple tool to deserialize data from an input encoding, transform it and serialize it back into an output encoding. Requires rust >= 1.56.0. Ins

null 11 Dec 14, 2022