Key paths provide a safe, composable way to access and modify nested data in Rust. Inspired by KeyPath and Functional Lenses system, this feature rich crate lets you work with struct fields and enum variants as first-class values.
- β Faster performance, better compiler optimizations
- β Write operations can be faster than manual unwrapping at deeper nesting levels
- β Zero runtime overhead
- β Better inlining - Compiler can optimize more aggressively
- β
Functional chains for
Arc<Mutex<T>>/Arc<RwLock<T>>- Compose keypaths through sync primitives - β parking_lot support - Optional feature for faster locks
- β
Tokio support - Async keypath chains through
Arc<tokio::sync::Mutex<T>>andArc<tokio::sync::RwLock<T>>
[dependencies]
rust-keypaths = "1.7.0"
keypaths-proc = "1.7.0"- β Readable/Writable keypaths for struct fields
- β
Failable keypaths for
Option<T>chains (_fr/_fw) - β Enum CasePaths (readable and writable prisms)
- β Composition across structs, options and enum cases
- β Iteration helpers over collections via keypaths
- β
Proc-macros:
#[derive(Keypaths)]for structs/tuple-structs and enums,#[derive(Casepaths)]for enums - β
Functional chains for
Arc<Mutex<T>>andArc<RwLock<T>>- Compose-first, apply-later pattern - β parking_lot support - Feature-gated support for faster synchronization primitives
- β
Tokio support - Async keypath chains through
Arc<tokio::sync::Mutex<T>>andArc<tokio::sync::RwLock<T>> - β Compile-time type safety - Invalid keypath compositions fail at compile time, preventing runtime errors
This example demonstrates keypath composition through deeply nested structures with Box<T> and enum variants:
use keypaths_proc::{Casepaths, Keypaths};
#[derive(Debug, Keypaths)]
#[Writable]
struct SomeComplexStruct {
scsf: Box<SomeOtherStruct>,
}
impl SomeComplexStruct {
fn new() -> Self {
Self {
scsf: Box::new(SomeOtherStruct {
sosf: OneMoreStruct {
omsf: String::from("no value for now"),
omse: SomeEnum::B(DarkStruct {
dsf: String::from("dark field"),
}),
},
}),
}
}
}
#[derive(Debug, Keypaths)]
#[Writable]
struct SomeOtherStruct {
sosf: OneMoreStruct,
}
#[derive(Debug, Casepaths)]
#[Writable]
enum SomeEnum {
A(String),
B(DarkStruct),
}
#[derive(Debug, Keypaths)]
#[Writable]
struct OneMoreStruct {
omsf: String,
omse: SomeEnum,
}
#[derive(Debug, Keypaths)]
#[Writable]
struct DarkStruct {
dsf: String,
}
fn main() {
use rust_keypaths::WritableOptionalKeyPath;
// Compose keypath through Box, nested structs, and enum variants
// Using .then() method (works on stable Rust)
let keypath = SomeComplexStruct::scsf_fw()
.then(SomeOtherStruct::sosf_fw())
.then(OneMoreStruct::omse_fw())
.then(SomeEnum::b_case_fw())
.then(DarkStruct::dsf_fw());
// Alternatively, use the >> operator (requires nightly feature):
// #![feature(impl_trait_in_assoc_type)]
// let keypath = SomeComplexStruct::scsf_fw()
// >> SomeOtherStruct::sosf_fw()
// >> OneMoreStruct::omse_fw()
// >> SomeEnum::b_case_fw()
// >> DarkStruct::dsf_fw();
let mut instance = SomeComplexStruct::new();
// Mutate deeply nested field through composed keypath
if let Some(dsf) = keypath.get_mut(&mut instance) {
*dsf = String::from("we can update the field of struct with the other way unlocked by keypaths");
println!("instance = {:?}", instance);
}
}Keypaths provide compile-time type safety - if you try to compose keypaths that don't share the same root type, the compiler will catch the error before your code runs.
The Rule: When chaining keypaths with .then(), the Value type of the first keypath must match the Root type of the second keypath.
use keypaths_proc::Keypaths;
#[derive(Keypaths)]
#[All]
struct Person {
name: String,
address: Address,
}
#[derive(Keypaths)]
#[All]
struct Address {
city: String,
}
#[derive(Keypaths)]
#[All]
struct Product {
name: String,
}
fn main() {
// β
CORRECT: Person -> Address -> city (all part of same hierarchy)
let city_kp = Person::address_r()
.then(Address::city_r());
// β COMPILE ERROR: Person::name_r() returns KeyPath<Person, String>
// Product::name_r() expects Product as root, not String!
// let invalid = Person::name_r()
// .then(Product::name_r()); // Error: expected `String`, found `Product`
}What happens:
- β Valid compositions compile successfully
- β Invalid compositions fail at compile time with clear error messages
- π‘οΈ No runtime errors - type mismatches are caught before execution
- π Clear error messages - Rust compiler shows exactly what types are expected vs. found
This ensures that keypath chains are always type-safe and prevents bugs that would only be discovered at runtime.
Running the example:
cargo run --example type_safety_demo
β οΈ IMPORTANT: When using the derive macro,MutexandRwLockdefault toparking_lotunless you explicitly usestd::sync::Mutexorstd::sync::RwLock.
[dependencies]
rust-keypaths = { version = "1.7.0", features = ["parking_lot"] }
keypaths-proc = "1.7.0"The derive macro generates helper methods for Arc<Mutex<T>> and Arc<RwLock<T>> fields:
| Field Type | Generated Methods | Description |
|---|---|---|
Arc<Mutex<T>> (parking_lot default) |
_r(), _w(), _fr_at(kp), _fw_at(kp) |
Chain through parking_lot::Mutex |
Arc<RwLock<T>> (parking_lot default) |
_r(), _w(), _fr_at(kp), _fw_at(kp) |
Chain through parking_lot::RwLock |
Arc<std::sync::Mutex<T>> |
_r(), _w(), _fr_at(kp), _fw_at(kp) |
Chain through std::sync::Mutex |
Arc<std::sync::RwLock<T>> |
_r(), _w(), _fr_at(kp), _fw_at(kp) |
Chain through std::sync::RwLock |
use std::sync::Arc;
use parking_lot::RwLock;
use keypaths_proc::Keypaths;
#[derive(Keypaths)]
#[Writable]
struct Container {
// This uses parking_lot::RwLock (default)
data: Arc<RwLock<DataStruct>>,
// This uses std::sync::RwLock (explicit)
std_data: Arc<std::sync::RwLock<DataStruct>>,
}
#[derive(Keypaths)]
#[Writable]
struct DataStruct {
name: String,
}
fn main() {
let container = Container { /* ... */ };
// Using generated _fr_at() for parking_lot (default)
Container::data_fr_at(DataStruct::name_r())
.get(&container, |value| {
println!("Name: {}", value);
});
// Using generated _fw_at() for parking_lot (default)
Container::data_fw_at(DataStruct::name_w())
.get_mut(&container, |value| {
*value = "New name".to_string();
});
// Using generated _fr_at() for std::sync::RwLock (explicit)
Container::std_data_fr_at(DataStruct::name_r())
.get(&container, |value| {
println!("Name: {}", value);
});
}Key advantage: parking_lot locks never fail (no poisoning), so chain methods don't return Option for the lock operation itself.
Running the example:
cargo run --example parking_lot_chains --features parking_lot
cargo run --example parking_lot_nested_chain --features parking_lot
β οΈ IMPORTANT: Tokio support requires thetokiofeature and usestokio::sync::Mutexandtokio::sync::RwLock. All operations are async and must be awaited.
[dependencies]
rust-keypaths = { version = "1.7.0", features = ["tokio"] }
keypaths-proc = "1.7.0"
tokio = { version = "1.38.0", features = ["sync", "rt", "rt-multi-thread", "macros"] }The derive macro generates helper methods for Arc<tokio::sync::Mutex<T>> and Arc<tokio::sync::RwLock<T>> fields:
| Field Type | Generated Methods | Description |
|---|---|---|
Arc<tokio::sync::Mutex<T>> |
_r(), _w(), _fr_at(kp), _fw_at(kp) |
Chain through tokio::sync::Mutex (async) |
Arc<tokio::sync::RwLock<T>> |
_r(), _w(), _fr_at(kp), _fw_at(kp) |
Chain through tokio::sync::RwLock (async, read/write locks) |
use std::sync::Arc;
use tokio::sync::{Mutex, RwLock};
use keypaths_proc::Keypaths;
#[derive(Keypaths)]
#[All] // Generate all methods (readable, writable, owned)
struct AppState {
user_data: Arc<tokio::sync::Mutex<UserData>>,
config: Arc<tokio::sync::RwLock<Config>>,
optional_cache: Option<Arc<tokio::sync::RwLock<Cache>>>,
}
#[derive(Keypaths)]
#[All]
struct UserData {
name: String,
email: String,
}
#[derive(Keypaths)]
#[All]
struct Config {
api_key: String,
timeout: u64,
}
#[derive(Keypaths)]
#[All]
struct Cache {
entries: Vec<String>,
size: usize,
}
#[tokio::main]
async fn main() {
let state = AppState { /* ... */ };
// Reading through Arc<tokio::sync::Mutex<T>> (async)
AppState::user_data_fr_at(UserData::name_r())
.get(&state, |name| {
println!("User name: {}", name);
})
.await;
// Writing through Arc<tokio::sync::Mutex<T>> (async)
AppState::user_data_fw_at(UserData::name_w())
.get_mut(&state, |name| {
*name = "Bob".to_string();
})
.await;
// Reading through Arc<tokio::sync::RwLock<T>> (async, read lock)
AppState::config_fr_at(Config::api_key_r())
.get(&state, |api_key| {
println!("API key: {}", api_key);
})
.await;
// Writing through Arc<tokio::sync::RwLock<T>> (async, write lock)
AppState::config_fw_at(Config::timeout_w())
.get_mut(&state, |timeout| {
*timeout = 60;
})
.await;
// Reading through optional Arc<tokio::sync::RwLock<T>> (async)
if let Some(()) = AppState::optional_cache_fr()
.chain_arc_tokio_rwlock_at_kp(Cache::size_r())
.get(&state, |size| {
println!("Cache size: {}", size);
})
.await
{
println!("Successfully read cache size");
}
}Key features:
- β Async operations: All lock operations are async and must be awaited
- β
Read/write locks:
RwLocksupports concurrent reads with_fr_at()and exclusive writes with_fw_at() - β
Optional chaining: Works seamlessly with
Option<Arc<tokio::sync::Mutex<T>>>andOption<Arc<tokio::sync::RwLock<T>>> - β Nested composition: Chain through multiple levels of Tokio locks and nested structures
Running the example:
cargo run --example tokio_containers --features tokioThe rust-key-paths library is being used by several exciting crates in the Rust ecosystem:
- π rust-queries-builder - Type-safe, SQL-like queries for in-memory collections
- π rust-overture - Functional programming utilities and abstractions
- π rust-prelude-plus - Enhanced prelude with additional utilities and traits
- π type-safe property paths
- π Swift KeyPath documentation
- π Elm Architecture & Functional Lenses
- π Rust Macros Book
- π Category Theory in FP (for intuition)
- Avoids repetitive
match/.chains. - Encourages compositional design.
- Plays well with DDD (Domain-Driven Design) and Actor-based systems.
- Useful for reflection-like behaviors in Rust (without unsafe).
- High performance: essentially zero overhead for deep nested writes (10 levels)!
KeyPaths are optimized for performance with minimal overhead. Below are benchmark results comparing direct unwrap vs keypaths for 10-level deep nested access:
| Operation | Direct Unwrap | KeyPath | Notes |
|---|---|---|---|
| Read (10 levels) | 384.07 ps | 848.27 ps | ~464 ps absolute difference |
| Write (10 levels) | 19.306 ns | 19.338 ns | Essentially identical! β‘ |
See benches/BENCHMARK_SUMMARY.md for detailed performance analysis.
The library includes comprehensive benchmarks for both parking_lot::RwLock and tokio::sync::RwLock operations:
parking_lot::RwLock benchmarks:
cargo bench --bench rwlock_write_deeply_nested --features parking_lotTokio RwLock benchmarks (read and write):
cargo bench --bench rwlock_write_deeply_nested --features parking_lot,tokioThe benchmarks compare:
- β
Keypath approach: Using
_fr_at()and_fw_at()methods for readable and writable access - βοΈ Traditional approach: Manual read/write guards with nested field access
Benchmarks include:
- Deeply nested read/write operations through
Arc<RwLock<T>> - Optional field access (
Option<T>) - Multiple sequential operations
- Both synchronous (
parking_lot) and asynchronous (tokio) primitives
Benchmark Results:
| Operation | Keypath | Manual Guard | Overhead | Notes |
|---|---|---|---|---|
| parking_lot::RwLock - Deep Write | 24.5 ns | 23.9 ns | 2.5% slower | Deeply nested write through Arc<RwLock<T>> |
| parking_lot::RwLock - Simple Write | 8.5 ns | 8.6 ns | 1.2% faster β‘ | Simple field write (Option<i32>) |
| parking_lot::RwLock - Field Write | 23.8 ns | 23.9 ns | 0.4% faster β‘ | Field write (Option<String>) |
| parking_lot::RwLock - Multiple Writes | 55.8 ns | 41.8 ns | 33.5% slower | Multiple sequential writes (single guard faster) |
| tokio::sync::RwLock - Deep Read | 104.8 ns | 104.6 ns | 0.2% slower | Deeply nested async read |
| tokio::sync::RwLock - Deep Write | 124.8 ns | 124.1 ns | 0.6% slower | Deeply nested async write |
| tokio::sync::RwLock - Simple Write | 103.8 ns | 105.0 ns | 1.2% faster β‘ | Simple async field write |
| tokio::sync::RwLock - Field Read | 103.3 ns | 103.2 ns | 0.1% slower | Simple async field read |
| tokio::sync::RwLock - Field Write | 125.7 ns | 124.6 ns | 0.9% slower | Simple async field write |
Key findings:
- β parking_lot::RwLock: Keypaths show essentially identical performance (0-2.5% overhead) for single operations
- β tokio::sync::RwLock: Keypaths show essentially identical performance (0-1% overhead) for async operations
- β‘ Simple operations: Keypaths can be faster than manual guards in some cases (1-2% improvement)
β οΈ Multiple writes: Manual single guard is faster (33% overhead) - use single guard for multiple operations- π― Type safety: Minimal performance cost for significant type safety and composability benefits
Detailed Analysis:
- For detailed performance analysis, see
benches/BENCHMARK_SUMMARY.md - For performance optimization details, see
benches/PERFORMANCE_ANALYSIS.md - For complete benchmark results, see
benches/BENCHMARK_RESULTS.md
| Feature | rust-keypaths | keypath | pl-lens | lens-rs |
|---|---|---|---|---|
| Struct Field Access | β Readable/Writable | β Readable/Writable | β Readable/Writable | β Partial |
| Option Chains | β
Built-in (_fr/_fw) |
β Manual composition | β Manual composition | β Manual |
| Enum Case Paths | β Built-in (CasePaths) | β Not supported | β Not supported | β Limited |
| Tuple Structs | β Full support | β Not supported | β Not supported | |
| Composition | β
.then() chaining |
|||
| Result<T, E> | β Built-in support | β Not supported | β Not supported | β Not supported |
| Mutex/RwLock | β
Built-in (with_mutex, etc.) |
β Not supported | β Not supported | β Not supported |
| Arc/Box/Rc | β Built-in support | |||
| Collections | β Vec, HashMap, HashSet, etc. | β Not supported | β Not supported | β Not supported |
| Derive Macros | β
#[derive(Keypaths)], #[derive(Casepaths)] |
β
#[derive(Keypath)] |
β
#[derive(Lenses)] |
|
| Deep Nesting | β Works seamlessly | β Requires workarounds | β Complex | |
| Type Safety | β Full compile-time checks | β Good | β Good | |
| Performance | β Optimized (1.46x overhead reads, near-zero writes) | |||
| Readable Keypaths | β
KeyPath |
β Supported | β
RefLens |
|
| Writable Keypaths | β
WritableKeyPath |
β Supported | β
Lens |
|
| Failable Readable | β
OptionalKeyPath |
β Manual | β Manual | β Manual |
| Failable Writable | β
WritableOptionalKeyPath |
β Manual | β Manual | β Manual |
| Zero-cost Abstractions | β | |||
| Swift KeyPath-like API | β Inspired by Swift | β No | β No | |
| Container Methods | β
with_mutex, with_rwlock, with_arc, etc. |
β Not supported | β Not supported | β Not supported |
| Iteration Helpers | β
iter(), iter_mut() |
β Not supported | β Not supported | β Not supported |
| Derivable References | β Full support | β Full support | β Not supported | β Not supported |
| Active Maintenance | β Active |
- β
Native Option support: Built-in failable keypaths (
_fr/_fw) that compose seamlessly throughOption<T>chains (unlike keypath, pl-lens, and lens-rs which require manual composition) - β
Enum CasePaths: First-class support for enum variant access (prisms) with
#[derive(Casepaths)](unique feature not found in keypath, pl-lens, or lens-rs) - β
Container types: Built-in support for
Result,Mutex,RwLock,Arc,Rc,Box, and all standard collections (comprehensive container support unmatched by alternatives) - β
Functional chains for sync primitives: Compose keypaths through
Arc<Mutex<T>>andArc<RwLock<T>>with a clean, functional API - β
parking_lot support: Feature-gated support for faster
parking_lot::Mutexandparking_lot::RwLock - β Zero-cost abstractions: Minimal overhead (1.46x for reads, near-zero for writes) - benchmarked and optimized
- β Comprehensive derive macros: Automatic generation for structs (named and tuple), enums, and all container types
- β
Swift-inspired API: Familiar API for developers coming from Swift's KeyPath system with
.then()composition - β Deep composition: Works seamlessly with 10+ levels of nesting without workarounds (tested and verified)
- β
Type-safe composition: Full compile-time type checking with
.then()method - β Active development: Regularly maintained with comprehensive feature set and documentation
- Inspired by Lenses: Compositional Data Access And Manipulation
- Compose across structs, options and enum cases
- Derive macros for automatic keypath generation (
Keypaths,Keypaths,Casepaths) - Optional chaining with failable keypaths
- Smart pointer adapters (
.for_arc(),.for_box(),.for_rc()) - Container support for
Result,Mutex,RwLock,Weak, and collections - Helper derive macros (
ReadableKeypaths,WritableKeypaths) - Functional chains for
Arc<Mutex<T>>andArc<RwLock<T>> -
parking_lotsupport for faster synchronization primitives - Tokio support for async keypath chains through
Arc<tokio::sync::Mutex<T>>andArc<tokio::sync::RwLock<T>> - Derive macros for complex multi-field enum variants
- Mozilla Public License 2.0