Agility
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.
Features
- ๐ Reactive Signals: Fine-grained reactive primitives with automatic dependency tracking
- ๐งต Thread-Safe Variant:
SignalSyncfor concurrent programming withSend + Syncsupport - ๐ฆ Composable Operations: Rich API with
map,combine,extend, and category-theory-inspired operations - ๐ฏ Type-Safe: Leverages Rust's type system for compile-time guarantees
- โก Efficient: Smart batching prevents redundant reactions during multiple updates
- ๐ Weak/Strong References: Control memory management with flexible reference strategies
- ๐๏ธ Derive Macros: Automatically lift structs containing signals with
#[derive(Lift)]and#[derive(LiftSync)] - ๐ญ Category Theory Concepts:
contramap,promapfor bidirectional data flow
Installation
Add this to your Cargo.toml:
[]
= "0.1.0"
Quick Start
use Signal;
// Create a signal with an initial value
let counter = new;
// Map the signal to create a derived signal
let doubled = counter.map;
// Observe changes with strong references
doubled.with;
// Update the signal - observers are notified automatically
counter.send; // Prints: "Counter doubled: 10"
Core Concepts
Signals
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 Signal;
let temperature = new;
let fahrenheit = temperature.map;
fahrenheit.with;
temperature.send; // Prints: "Temperature: 77ยฐF"
Weak vs Strong References
Agility provides two strategies for managing signal lifetimes:
-
map(): Creates derived signals with weak references- The derived signal doesn't keep the source alive
- Important: You must keep a binding (
let _observer = ...) for reactions to fire - Without a binding, the signal is immediately dropped and won't propagate changes
-
with(): Creates derived signals with strong references- The derived signal keeps the source alive
- The binding keeps everything in the dependency chain alive
- Use when you need guaranteed lifetime management
let source = new;
// โ Wrong: reaction never fires (immediately dropped)
source.map;
// โ
Correct: keep the binding alive
let _observer = source.map;
// โ
Strong reference: also keeps the binding
source.with;
Batching Updates
Signal guards enable batching multiple updates to prevent redundant reactions:
let a = new;
let b = new;
let sum = a.combine.map;
sum.with;
// Batch updates - reaction fires only once
; // Prints: "Sum: 30" (only once)
Advanced Features
Combining Signals
Combine multiple signals into compound values:
use Signal;
let first_name = new;
let last_name = new;
let full_name = first_name.combine
.map;
full_name.with;
first_name.send; // Prints: "Full name: Jane Doe"
Lifting Collections
Lift arrays or vectors of signals into a single signal:
use ;
let x = new;
let y = new;
let z = new;
// Lift array of signals
let coords = .lift;
coords.with;
x.send; // Prints: "Coordinates: (10, 2, 3)"
// Lift tuple of signals
let point = .lift;
point.with;
Extending Signals
Extend a signal with additional signals to create a vector:
let first = new;
let second = new;
let third = new;
let all = first.extend;
all.with;
first.send; // Prints: "All values: [10, 2, 3]"
Category Theory Operations
Contravariant Mapping
Flow data backwards from derived to source:
let result = new;
let source = result.contramap;
result.with;
source.with;
source.send; // Prints: "Source: 100" then "Result: 200"
Profunctor (Bidirectional) Mapping
Create bidirectional data flow between signals:
let celsius = new;
let fahrenheit = celsius.promap;
celsius.with;
fahrenheit.with;
celsius.send; // Prints both values
fahrenheit.send; // Prints both values (0ยฐC)
Signal Dependencies
Make one signal depend on another:
let master = new;
let follower = new;
follower.depend;
follower.with;
master.send; // Prints: "Follower: 42"
Thread-Safe Signals
For concurrent programming, use SignalSync:
use SignalSync;
use thread;
let counter = new;
let doubled = counter.map;
doubled.with;
let counter_clone = counter.clone;
spawn.join.unwrap;
// Prints: "Value: 20"
Derive Macros
Automatically lift structs containing signals:
use ;
let state = AppState ;
let lifted = state.lift; // Signal<'a, _AppState>
lifted.with;
For thread-safe structs, use #[derive(LiftSync)]:
use ;
Performance Considerations
- Automatic Cleanup: Weak references allow unused signals to be garbage collected
- Batch Updates: Use tuples
(signal1.send(x), signal2.send(y))to batch updates - Strong References: Use
with()andand()when you need to keep signals alive - Thread Safety:
SignalSyncusesArc,Mutex, andRwLockfor thread-safe operations
Comparison with Other Libraries
| 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 |
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under either of
- Apache License, Version 2.0, (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Contribution
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.
Inspiration
Agility is inspired by:
- Reactive programming concepts from functional languages
- Category theory (functors, contravariant functors, profunctors)
- Fine-grained reactivity systems like SolidJS and Leptos
- The need for a flexible, composable reactive library in Rust
Changelog
0.1.0 (Initial Release)
- Single-threaded
Signalwith automatic dependency tracking - Thread-safe
SignalSyncfor concurrent programming - Rich API with map, combine, extend operations
- Category theory operations: contramap, promap
- Derive macros for automatic struct lifting
- Weak and strong reference strategies
- Batch update support with signal guards