|
3 | 3 | Key paths provide a **safe, composable way to access and modify nested data** in Rust. |
4 | 4 | Inspired by **KeyPath and Functional Lenses** system, this feature rich crate lets you work with **struct fields** and **enum variants** as *first-class values*. |
5 | 5 |
|
| 6 | +## Starter Guide |
| 7 | + |
| 8 | +### Installation |
| 9 | + |
| 10 | +Add to your `Cargo.toml`: |
| 11 | + |
| 12 | +```toml |
| 13 | +[dependencies] |
| 14 | +rust-key-paths = "2.0.6" |
| 15 | +key-paths-derive = "2.0.6" |
| 16 | +``` |
| 17 | + |
| 18 | +### Basic usage |
| 19 | + |
| 20 | +```rust |
| 21 | +use std::sync::Arc; |
| 22 | +use key_paths_derive::Kp; |
| 23 | + |
| 24 | +#[derive(Debug, Kp)] |
| 25 | +struct SomeComplexStruct { |
| 26 | + scsf: Option<SomeOtherStruct>, |
| 27 | + scfs2: Arc<std::sync::RwLock<SomeOtherStruct>>, |
| 28 | +} |
| 29 | + |
| 30 | +#[derive(Debug, Kp)] |
| 31 | +struct SomeOtherStruct { |
| 32 | + sosf: Option<OneMoreStruct>, |
| 33 | +} |
| 34 | + |
| 35 | +#[derive(Debug, Kp)] |
| 36 | +enum SomeEnum { |
| 37 | + A(String), |
| 38 | + B(Box<DarkStruct>), |
| 39 | +} |
| 40 | + |
| 41 | +#[derive(Debug, Kp)] |
| 42 | +struct OneMoreStruct { |
| 43 | + omsf: Option<String>, |
| 44 | + omse: Option<SomeEnum>, |
| 45 | +} |
| 46 | + |
| 47 | +#[derive(Debug, Kp)] |
| 48 | +struct DarkStruct { |
| 49 | + dsf: Option<String>, |
| 50 | +} |
| 51 | + |
| 52 | +impl SomeComplexStruct { |
| 53 | + fn new() -> Self { |
| 54 | + Self { |
| 55 | + scsf: Some(SomeOtherStruct { |
| 56 | + sosf: Some(OneMoreStruct { |
| 57 | + omsf: Some(String::from("no value for now")), |
| 58 | + omse: Some(SomeEnum::B(Box::new(DarkStruct { |
| 59 | + dsf: Some(String::from("dark field")), |
| 60 | + }))), |
| 61 | + }), |
| 62 | + }), |
| 63 | + scfs2: Arc::new(std::sync::RwLock::new(SomeOtherStruct { |
| 64 | + sosf: Some(OneMoreStruct { |
| 65 | + omsf: Some(String::from("no value for now")), |
| 66 | + omse: Some(SomeEnum::B(Box::new(DarkStruct { |
| 67 | + dsf: Some(String::from("dark field")), |
| 68 | + }))), |
| 69 | + }), |
| 70 | + })), |
| 71 | + } |
| 72 | + } |
| 73 | +} |
| 74 | +fn main() { |
| 75 | + let mut instance = SomeComplexStruct::new(); |
| 76 | + |
| 77 | + SomeComplexStruct::scsf() |
| 78 | + .then(SomeOtherStruct::sosf()) |
| 79 | + .then(OneMoreStruct::omse()) |
| 80 | + .then(SomeEnum::b()) |
| 81 | + .then(DarkStruct::dsf()) |
| 82 | + .get_mut(&mut instance).map(|x| { |
| 83 | + *x = String::from("🖖🏿🖖🏿🖖🏿🖖🏿"); |
| 84 | + }); |
| 85 | + |
| 86 | + println!("instance = {:?}", instance.scsf.unwrap().sosf.unwrap().omse.unwrap()); |
| 87 | + // output - instance = B(DarkStruct { dsf: Some("🖖🏿🖖🏿🖖🏿🖖🏿") }) |
| 88 | +} |
| 89 | +``` |
| 90 | + |
| 91 | +### Composing keypaths |
| 92 | + |
| 93 | +Chain through nested structures with `then()`: |
| 94 | + |
| 95 | +```rust |
| 96 | +#[derive(Kp)] |
| 97 | +struct Address { street: String } |
| 98 | + |
| 99 | +#[derive(Kp)] |
| 100 | +struct Person { address: Box<Address> } |
| 101 | + |
| 102 | +let street_kp = Person::address().then(Address::street()); |
| 103 | +let street = street_kp.get(&person); // Option<&String> |
| 104 | +``` |
| 105 | + |
| 106 | +### Partial and Any keypaths |
| 107 | + |
| 108 | +Use `#[derive(Pkp, Akp)]` (requires `Kp`) to get type-erased keypath collections: |
| 109 | + |
| 110 | +- **PKp** – `partial_kps()` returns `Vec<PKp<Self>>`; value type erased, root known |
| 111 | +- **AKp** – `any_kps()` returns `Vec<AKp>`; both root and value type-erased for heterogeneous collections |
| 112 | + |
| 113 | +Filter by `value_type_id()` / `root_type_id()` and read with `get_as()`. For writes, dispatch to the typed `Kp` (e.g. `Person::name()`) based on TypeId. |
| 114 | + |
| 115 | +See examples: `pkp_akp_filter_typeid`, `pkp_akp_read_write_convert`. |
| 116 | + |
| 117 | +### GPU / wgpu (key-paths-iter, optional) |
| 118 | + |
| 119 | +The [key-paths-iter](https://github.com/codefonsi/rust-key-paths) crate can run **numeric** keypaths (e.g. `f32`) on the GPU via wgpu. Two styles: |
| 120 | + |
| 121 | +- **AKp runner** (`wgpu` module): `IntoNumericAKp` from Kp, `AKpTier::Numeric` / `Arbitrary`, `AKpRunner`. Examples: `kp_pkp_wgpu`, `akp_wgpu_runner`. |
| 122 | +- **Kp-only** (`kp_gpu` module): no AKp/PKp — `.map_gpu(wgsl)`, `.par_gpu(wgsl, roots, ctx)`, `GpuKpRunner`. Examples: `kp_gpu_example`, `kp_gpu_vec_example`, `kp_gpu_practical_app` (finance: Monte Carlo, batch options, stress-test). Kp with value `Vec<V>`: `.map_gpu_vec(wgsl)` for one dispatch over the vector. |
| 123 | + |
| 124 | +Run benchmarks: `cargo bench --bench akp_cpu_bench`. Typical results (MacBook Air M1): |
| 125 | + |
| 126 | +| Roots | Serial (CPU) | Parallel CPU (Rayon) | Parallel GPU (wgpu) | |
| 127 | +|--------|---------------|----------------------|---------------------| |
| 128 | +| 1,000 | ~35 µs | ~86 µs | ~1.6 ms | |
| 129 | +| 10,000 | ~350 µs | ~425 µs | ~1.9 ms | |
| 130 | +| 50,000 | ~1.8 ms | ~1.8 ms | ~3.7 ms | |
| 131 | +| 100,000| ~3.7 ms | ~3.7 ms | ~5.5 ms | |
| 132 | + |
| 133 | +For this lightweight transform, CPU wins; GPU pays off for larger batches or heavier per-element math. |
| 134 | + |
| 135 | +### Features |
| 136 | + |
| 137 | +| Feature | Description | |
| 138 | +|---------|-------------| |
| 139 | +| `parking_lot` | Use `parking_lot::Mutex` / `RwLock` instead of `std::sync` | |
| 140 | +| `tokio` | Async lock support (`tokio::sync::Mutex`, `RwLock`) | |
| 141 | +| `pin_project` | Enable `#[pin]` field support for pin-project compatibility | |
| 142 | + |
| 143 | +### More examples |
| 144 | + |
| 145 | +```bash |
| 146 | +cargo run --example kp_derive_showcase |
| 147 | +cargo run --example pkp_akp_filter_typeid |
| 148 | +cargo run --example pkp_akp_read_write_convert |
| 149 | +# Kp/Pkp + wgpu (key-paths-iter with gpu feature) |
| 150 | +cargo run --example kp_pkp_wgpu |
| 151 | +cargo run --example akp_wgpu_runner |
| 152 | +cargo run --example kp_gpu_example |
| 153 | +cargo run --example kp_gpu_vec_example |
| 154 | +cargo run --example kp_gpu_practical_app |
| 155 | +# Box and Pin support |
| 156 | +cargo run --example box_and_pin_example |
| 157 | +# pin_project #[pin] fields |
| 158 | +cargo run --example pin_project_example --features pin_project |
| 159 | +cargo run --example pin_project_fair_race --features "pin_project,tokio" |
| 160 | +# Deadlock prevention (parallel execution) |
| 161 | +cargo run --example deadlock_prevention_sync --features parking_lot |
| 162 | +cargo run --example deadlock_prevention_async --features tokio |
| 163 | +``` |
| 164 | + |
| 165 | +--- |
| 166 | + |
| 167 | +## Supported containers |
| 168 | + |
| 169 | +The `#[derive(Kp)]` macro (from `key-paths-derive`) generates keypath accessors for these wrapper types: |
| 170 | + |
| 171 | +| Container | Access | Notes | |
| 172 | +|-----------|--------|-------| |
| 173 | +| `Option<T>` | `field()` | Unwraps to inner type | |
| 174 | +| `Box<T>` | `field()` | Derefs to inner | |
| 175 | +| `Pin<T>`, `Pin<Box<T>>` | `field()`, `field_inner()` | Container + inner (when `T: Unpin`) | |
| 176 | +| `Rc<T>`, `Arc<T>` | `field()` | Derefs; mut when unique ref | |
| 177 | +| `Vec<T>` | `field()`, `field_at(i)` | Container + index access | |
| 178 | +| `HashMap<K,V>`, `BTreeMap<K,V>` | `field_at(k)` | Key-based access | |
| 179 | +| `HashSet<T>`, `BTreeSet<T>` | `field()` | Container identity | |
| 180 | +| `VecDeque<T>`, `LinkedList<T>`, `BinaryHeap<T>` | `field()`, `field_at(i)` | Index where applicable | |
| 181 | +| `Result<T,E>` | `field()` | Unwraps `Ok` | |
| 182 | +| `Cow<'_, T>` | `field()` | `as_ref` / `to_mut` | |
| 183 | +| `Option<Cow<'_, T>>` | `field()` | Optional Cow unwrap | |
| 184 | +| `std::sync::Mutex<T>`, `std::sync::RwLock<T>` | `field()` | Container (use `LockKp` for lock-through) | |
| 185 | +| `Arc<Mutex<T>>`, `Arc<RwLock<T>>` | `field()`, `field_lock()` | Lock-through via `LockKp` | |
| 186 | +| `tokio::sync::Mutex`, `tokio::sync::RwLock` | `field_async()` | Async lock-through (tokio feature) | |
| 187 | +| `parking_lot::Mutex`, `parking_lot::RwLock` | `field()`, `field_lock()` | parking_lot feature | |
| 188 | + |
| 189 | +Nested combinations (e.g. `Option<Box<T>>`, `Option<Vec<T>>`, `Vec<Option<T>>`) are supported. |
| 190 | + |
| 191 | +### pin_project `#[pin]` fields (optional feature) |
| 192 | + |
| 193 | +When using [pin-project](https://docs.rs/pin-project), mark pinned fields with `#[pin]`. The derive generates: |
| 194 | + |
| 195 | +| `#[pin]` field type | Access | Notes | |
| 196 | +|---------------------|--------|-------| |
| 197 | +| Plain (e.g. `i32`) | `field()`, `field_pinned()` | Pinned projection via `this.project()` | |
| 198 | +| `Future` | `field()`, `field_pinned()`, `field_await()` | Poll through `Pin<&mut Self>` | |
| 199 | +| `Box<dyn Future<Output=T>>` | `field()`, `field_pinned()`, `field_await()` | Same for boxed futures | |
| 200 | + |
| 201 | +Enable with `pin_project` feature and add `#[pin_project]` to your struct: |
| 202 | + |
| 203 | +```rust |
| 204 | +#[pin_project] |
| 205 | +#[derive(Kp)] |
| 206 | +struct WithPinnedFuture { |
| 207 | + fair: bool, |
| 208 | + #[pin] |
| 209 | + fut: Pin<Box<dyn Future<Output = String> + Send>>, |
| 210 | +} |
| 211 | +``` |
| 212 | + |
| 213 | +Examples: `pin_project_example`, `pin_project_fair_race` (FairRaceFuture use case). |
| 214 | + |
| 215 | +## Performance: Kp vs direct unwrap |
| 216 | + |
| 217 | +Benchmark: nested `Option` chains and enum case paths (`cargo bench --bench keypath_vs_unwrap`). |
| 218 | + |
| 219 | +| Scenario | Keypath | Direct unwrap | Overhead | |
| 220 | +|----------|---------|---------------|----------| |
| 221 | +| 100× reuse (3-level) | ~36.6 ns | ~36.7 ns | ~1x | |
| 222 | +| 100× reuse (5-level) | ~52.3 ns | ~52.5 ns | ~1x | |
| 223 | + |
| 224 | +Access overhead comes from closure indirection in the composed chain. **Reusing a keypath** (build once, use many times) matches direct unwrap; building the chain each time adds ~1–2 ns. |
| 225 | + |
| 226 | +### Would static keypaths help? |
| 227 | + |
| 228 | +Yes. Static/const keypaths would: |
| 229 | +- Remove creation cost entirely (no closure chain construction per use) |
| 230 | +- Allow the compiler to inline the full traversal |
| 231 | +- Likely close the gap to near-zero overhead vs manual unwrap |
| 232 | + |
| 233 | +Currently, `Kp::then()` composes via closures that capture the previous step, so each access goes through a chain of function calls. A static keypath could flatten this to direct field offsets. |
| 234 | + |
| 235 | +--- |
| 236 | + |
| 237 | +## Performance: LockKp (Arc<Mutex>, Arc<RwLock>) |
| 238 | + |
6 | 239 | | Operation | Keypath | Direct Locks | Overhead | |
7 | 240 | |-----------|---------|--------------|----------| |
8 | 241 | | **Read** | ~241 ns | ~117 ns | ~2.1x | |
9 | 242 | | **Write** | ~239 ns | ~114 ns | ~2.1x | |
10 | 243 |
|
11 | | -The keypath approach builds the chain each iteration and traverses through `LockKp.then().then().then_async().then()`; direct locks use `sync_mutex.lock()` then `tokio_mutex.lock().await`. The keypath overhead reflects chain construction plus composed traversal vs. manual lock nesting. Hot-path functions are annotated with `#[inline]` for improved performance. |
| 244 | +The keypath approach builds the chain each iteration and traverses through `LockKp.then().then().then_async().then()`; direct locks use `sync_mutex.lock()` then `tokio_mutex.lock().await`. Hot-path functions are annotated with `#[inline]` for improved performance. |
| 245 | + |
| 246 | +### 10-level deep Arc<RwLock> benchmarks (leaf: f64) |
| 247 | + |
| 248 | +Benchmark: 10 levels of nested `Arc<RwLock<Next>>`, reading/writing leaf `f64`. Run with: |
| 249 | +- `cargo bench --features parking_lot --bench ten_level_arc_rwlock` |
| 250 | +- `cargo bench --bench ten_level_std_rwlock` |
| 251 | +- `cargo bench --features tokio --bench ten_level_tokio_rwlock` |
| 252 | + |
| 253 | +**Incr** (write: leaf += 0.25): |
| 254 | + |
| 255 | +| RwLock implementation | keypath_static | keypath_dynamic | direct_lock | |
| 256 | +|-----------------------|----------------|-----------------|-------------| |
| 257 | +| **parking_lot** | ~34 ns | ~41 ns | ~39 ns | |
| 258 | +| **std::sync** | ~46 ns | ~54 ns | ~46 ns | |
| 259 | +| **tokio::sync** | ~1.79 µs | ~1.78 µs | ~278 ns | |
| 260 | + |
| 261 | +Static keypath (chain built once, reused) matches or beats direct lock for sync RwLocks. For tokio, async keypath has higher overhead than direct `.read().await`/`.write().await`; direct lock is fastest. |
12 | 262 |
|
13 | 263 | --- |
14 | 264 |
|
15 | 265 | ## 📜 License |
16 | 266 |
|
17 | | -* Mozilla Public License 2.0 |
| 267 | +* Mozilla Public License 2.0 |
0 commit comments