| Crates.io | agility |
| lib.rs | agility |
| version | 0.1.1 |
| created_at | 2025-11-03 14:25:31.633575+00 |
| updated_at | 2025-11-04 12:25:50.009603+00 |
| description | A powerful and elegant reactive programming library for Rust, inspired by category theory |
| homepage | https://github.com/ICmd-dev/agility |
| repository | https://github.com/ICmd-dev/agility |
| max_upload_size | |
| id | 1914709 |
| size | 104,191 |
A powerful and elegant reactive programming library for Rust, inspired by category theory concepts. Agility provides composable, type-safe signals for building reactive systems with both single-threaded and thread-safe variants.
SignalSync for concurrent programming with Send + Sync supportmap, combine, extend, and category-theory-inspired operations#[derive(Lift)] and #[derive(LiftSync)]contramap, promap for bidirectional data flowAdd this to your Cargo.toml:
[dependencies]
agility = "0.1.0"
use agility::Signal;
// Create a signal with an initial value
let counter = Signal::new(0);
// Map the signal to create a derived signal
let doubled = counter.map(|x| x * 2);
// Observe changes with strong references
doubled.with(|x| println!("Counter doubled: {}", x));
// Update the signal - observers are notified automatically
counter.send(5); // Prints: "Counter doubled: 10"
A Signal<'a, T> represents a reactive value that can change over time. When a signal's value changes, all dependent signals are automatically updated.
use agility::Signal;
let temperature = Signal::new(20);
let fahrenheit = temperature.map(|c| c * 9 / 5 + 32);
fahrenheit.with(|f| println!("Temperature: {}ยฐF", f));
temperature.send(25); // Prints: "Temperature: 77ยฐF"
Agility provides two strategies for managing signal lifetimes:
map(): Creates derived signals with weak references
let _observer = ...) for reactions to firewith(): Creates derived signals with strong references
let source = Signal::new(10);
// โ Wrong: reaction never fires (immediately dropped)
source.map(|x| println!("Value: {}", x));
// โ
Correct: keep the binding alive
let _observer = source.map(|x| println!("Value: {}", x));
// โ
Strong reference: also keeps the binding
source.with(|x| println!("Value: {}", x));
Signal guards enable batching multiple updates to prevent redundant reactions:
let a = Signal::new(1);
let b = Signal::new(2);
let sum = a.combine(&b).map(|(x, y)| x + y);
sum.with(|total| println!("Sum: {}", total));
// Batch updates - reaction fires only once
(a.send(10), b.send(20)); // Prints: "Sum: 30" (only once)
Combine multiple signals into compound values:
use agility::Signal;
let first_name = Signal::new("John".to_string());
let last_name = Signal::new("Doe".to_string());
let full_name = first_name.combine(&last_name)
.map(|(first, last)| format!("{} {}", first, last));
full_name.with(|name| println!("Full name: {}", name));
first_name.send("Jane".to_string()); // Prints: "Full name: Jane Doe"
Lift arrays or vectors of signals into a single signal:
use agility::{Signal, LiftInto};
let x = Signal::new(1);
let y = Signal::new(2);
let z = Signal::new(3);
// Lift array of signals
let coords = [&x, &y, &z].lift();
coords.with(|[a, b, c]| println!("Coordinates: ({}, {}, {})", a, b, c));
x.send(10); // Prints: "Coordinates: (10, 2, 3)"
// Lift tuple of signals
let point = (&x, &y).lift();
point.with(|(a, b)| println!("Point: ({}, {})", a, b));
Extend a signal with additional signals to create a vector:
let first = Signal::new(1);
let second = Signal::new(2);
let third = Signal::new(3);
let all = first.extend(vec![second, third]);
all.with(|values| println!("All values: {:?}", values));
first.send(10); // Prints: "All values: [10, 2, 3]"
Flow data backwards from derived to source:
let result = Signal::new(42);
let source = result.contramap(|x| x * 2);
result.with(|x| println!("Result: {}", x));
source.with(|x| println!("Source: {}", x));
source.send(100); // Prints: "Source: 100" then "Result: 200"
Create bidirectional data flow between signals:
let celsius = Signal::new(0);
let fahrenheit = celsius.promap(
|c| c * 9 / 5 + 32, // Forward: C -> F
|f| (f - 32) * 5 / 9 // Backward: F -> C
);
celsius.with(|c| println!("Celsius: {}", c));
fahrenheit.with(|f| println!("Fahrenheit: {}", f));
celsius.send(100); // Prints both values
fahrenheit.send(32); // Prints both values (0ยฐC)
Make one signal depend on another:
let master = Signal::new(10);
let follower = Signal::new(0);
follower.depend(&master);
follower.with(|x| println!("Follower: {}", x));
master.send(42); // Prints: "Follower: 42"
For concurrent programming, use SignalSync:
use agility::SignalSync;
use std::thread;
let counter = SignalSync::new(0);
let doubled = counter.map(|x| x * 2);
doubled.with(|x| println!("Value: {}", x));
let counter_clone = counter.clone();
thread::spawn(move || {
counter_clone.send(10);
}).join().unwrap();
// Prints: "Value: 20"
Automatically lift structs containing signals:
use agility::{Signal, Lift};
#[derive(Lift)]
struct AppState<'a> {
counter: Signal<'a, i32>,
name: String,
}
let state = AppState {
counter: Signal::new(0),
name: "App".to_string(),
};
let lifted = state.lift(); // Signal<'a, _AppState>
lifted.with(|s| println!("Counter: {}, Name: {}", s.counter, s.name));
For thread-safe structs, use #[derive(LiftSync)]:
use agility::{SignalSync, LiftSync};
#[derive(LiftSync)]
struct ThreadSafeState<'a> {
value: SignalSync<'a, i32>,
label: String,
}
(signal1.send(x), signal2.send(y)) to batch updateswith() and and() when you need to keep signals aliveSignalSync uses Arc, Mutex, and RwLock for thread-safe operations| Feature | Agility | Other Reactive Libs |
|---|---|---|
| Weak References | โ Built-in | โ Usually not supported |
| Thread-Safe Variant | โ
SignalSync |
โ ๏ธ Varies |
| Category Theory Ops | โ
contramap, promap |
โ Rare |
| Derive Macros | โ Auto-lift structs | โ ๏ธ Limited |
| Batch Updates | โ Signal guards | โ ๏ธ Manual |
| Type Safety | โ Compile-time | โ Varies |
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under either of
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
Agility is inspired by: