The keypath library now supports seamless interoperability between three keypath types, allowing you to chain through different lock levels and regular field access in a single navigation path:
Kp: Regular field access (no locks)LockKp: Synchronous locks (Arc<Mutex>,Arc<RwLock>,Rc<RefCell>)AsyncLockKp: Asynchronous locks (Arc<tokio::sync::Mutex>,Arc<tokio::sync::RwLock>)
AsyncLockKp (top level - async locks)
↓
LockKp (middle level - sync locks)
↓
Kp (bottom level - regular fields)
Navigate through an async lock, then continue with regular field access.
pub async fn then<'a, V2, Value2, MutValue2, G3, S3>(
&'a self,
next_kp: &'a crate::Kp<V, V2, Value, Value2, MutValue, MutValue2, G3, S3>,
root: Root,
) -> Option<Value2>Example:
use tokio::sync::Mutex;
struct Root {
data: Arc<Mutex<Inner>>,
}
struct Inner {
value: i32,
}
// Create AsyncLockKp to navigate to Inner
let async_kp = AsyncLockKp::new(
Kp::new(|r: &Root| Some(&r.data), ...), // prev: Root -> Arc<Mutex<Inner>>
TokioMutexAccess::new(), // mid: unlock Inner
Kp::new(|i: &Inner| Some(i), ...), // next: Inner -> Inner
);
// Create Kp to navigate to value field
let value_kp = Kp::new(
|i: &Inner| Some(&i.value),
|i: &mut Inner| Some(&mut i.value),
);
// Chain: async lock -> field access
let result = async_kp.then(&value_kp, &root).await;
// result == Some(&42)Navigate through multiple nested async locks.
Note: Due to Rust's closure Clone constraints, this method takes the components of the second AsyncLockKp (prev, mid, next) separately rather than the struct itself.
pub async fn later_then<'a, Lock2, Mid2, V2, ...>(
&'a self,
other_prev: crate::Kp<V, Lock2, ...>,
other_mid: L2,
other_next: crate::Kp<Mid2, V2, ...>,
root: Root,
) -> Option<Value2>Example:
use tokio::sync::Mutex;
struct Root {
lock1: Arc<Mutex<Container>>,
}
struct Container {
lock2: Arc<Mutex<i32>>,
}
// First AsyncLockKp: Root -> Container
let async_kp1 = AsyncLockKp::new(
Kp::new(|r: &Root| Some(&r.lock1), ...),
TokioMutexAccess::new(),
Kp::new(|c: &Container| Some(c), ...),
);
// Second AsyncLockKp: Container -> i32
let async_kp2 = AsyncLockKp::new(
Kp::new(|c: &Container| Some(&c.lock2), ...),
TokioMutexAccess::new(),
Kp::new(|n: &i32| Some(n), ...),
);
// Compose: async lock1 -> async lock2
// Pass components separately to avoid Clone bounds on closures
let result = async_kp1.later_then(
async_kp2.prev,
async_kp2.mid,
async_kp2.next,
&root
).await;
// result == Some(&999)Navigate through an async lock, then through a sync lock.
pub async fn then_lock_kp_get<'a, Lock2, ...>(
&'a self,
lock_kp: &'a crate::lock::LockKp<...>,
root: Root,
) -> Option<Value2>Example:
use tokio::sync::Mutex as TokioMutex;
use std::sync::{Arc as StdArc, Mutex as StdMutex};
struct Root {
async_lock: Arc<TokioMutex<Container>>,
}
struct Container {
sync_lock: StdArc<StdMutex<i32>>,
}
let async_kp = AsyncLockKp::new(...); // Root -> Container
let lock_kp = LockKp::new(...); // Container -> i32
// Chain: async lock -> sync lock
let result = async_kp.then_lock_kp_get(&lock_kp, &root).await;Navigate through multiple async locks.
pub async fn compose_async_get<'a, Lock2, ...>(
&'a self,
other: &'a AsyncLockKp<...>,
root: Root,
) -> Option<Value2>Example:
struct Root {
lock1: Arc<tokio::sync::Mutex<Container>>,
}
struct Container {
lock2: Arc<tokio::sync::Mutex<i32>>,
}
let async_kp1 = AsyncLockKp::new(...); // Root -> Container
let async_kp2 = AsyncLockKp::new(...); // Container -> i32
// Chain: async lock1 -> async lock2
let result = async_kp1.compose_async_get(&async_kp2, &root).await;Navigate through a sync lock, then through an async lock.
#[cfg(feature = "tokio")]
pub async fn then_async_kp_get<'a, Lock2, ...>(
&'a self,
async_kp: &'a crate::async_lock::AsyncLockKp<...>,
root: Root,
) -> Option<Value2>Example:
use std::sync::{Arc, Mutex};
use tokio::sync::Mutex as TokioMutex;
struct Root {
sync_lock: Arc<Mutex<Container>>,
}
struct Container {
async_lock: Arc<TokioMutex<String>>,
}
let lock_kp = LockKp::new(...); // Root -> Container
let async_kp = AsyncLockKp::new(...); // Container -> String
// Chain: sync lock -> async lock
let result = lock_kp.then_async_kp_get(&async_kp, &root).await;Navigate to a field, then through a sync lock.
pub fn then_lock_kp_get<Lock, Mid, V2, ...>(
&self,
lock_kp: &crate::lock::LockKp<...>,
root: Root,
) -> Option<Value2>Example:
struct Root {
container: Container,
}
struct Container {
sync_lock: Arc<Mutex<i32>>,
}
let regular_kp: KpType<Root, Container> = Kp::new(
|r: &Root| Some(&r.container),
|r: &mut Root| Some(&mut r.container),
);
let lock_kp = LockKp::new(...); // Container -> i32
// Chain: regular field -> sync lock
let result = regular_kp.then_lock_kp_get(&lock_kp, &root);Navigate to a field, then through an async lock.
#[cfg(feature = "tokio")]
pub async fn then_async_kp_get<Lock, Mid, V2, ...>(
&self,
async_kp: &crate::async_lock::AsyncLockKp<...>,
root: Root,
) -> Option<Value2>Example:
struct Root {
container: Container,
}
struct Container {
async_lock: Arc<tokio::sync::Mutex<String>>,
}
let regular_kp: KpType<Root, Container> = Kp::new(
|r: &Root| Some(&r.container),
|r: &mut Root| Some(&mut r.container),
);
let async_kp = AsyncLockKp::new(...); // Container -> String
// Chain: regular field -> async lock
let result = regular_kp.then_async_kp_get(&async_kp, &root).await;use std::sync::{Arc as StdArc, Mutex as StdMutex};
use tokio::sync::Mutex as TokioMutex;
struct Root {
level1: Level1, // Regular field
}
struct Level1 {
sync_lock: StdArc<StdMutex<Level2>>, // Sync lock
}
struct Level2 {
async_lock: Arc<TokioMutex<i32>>, // Async lock
}
#[tokio::main]
async fn main() {
let root = Root {
level1: Level1 {
sync_lock: StdArc::new(StdMutex::new(Level2 {
async_lock: Arc::new(TokioMutex::new(888)),
})),
},
};
// Step 1: Regular Kp to navigate to Level1
let kp: KpType<Root, Level1> = Kp::new(
|r: &Root| Some(&r.level1),
|r: &mut Root| Some(&mut r.level1),
);
// Step 2: LockKp through sync lock to Level2
let lock_kp = LockKp::new(
Kp::new(|l1: &Level1| Some(&l1.sync_lock), ...),
ArcMutexAccess::new(),
Kp::new(|l2: &Level2| Some(l2), ...),
);
// Step 3: AsyncLockKp through async lock to i32
let async_kp = AsyncLockKp::new(
Kp::new(|l2: &Level2| Some(&l2.async_lock), ...),
TokioMutexAccess::new(),
Kp::new(|n: &i32| Some(n), ...),
);
// Chain all three: Kp -> LockKp -> AsyncLockKp
let level1 = kp.get(&root).unwrap();
let level2 = lock_kp.get(level1).unwrap();
let result = async_kp.get_async(level2).await;
assert_eq!(result, Some(&888));
}| From | To | Method Name | Description |
|---|---|---|---|
| AsyncLockKp | Kp | then() |
Chain to regular field |
| AsyncLockKp | AsyncLockKp | later_then() |
Compose with another async lock |
| AsyncLockKp | LockKp | then_lock_kp_get() |
Chain to sync lock |
| AsyncLockKp | AsyncLockKp | compose_async_get() |
Chain to another async lock (alternative) |
| LockKp | AsyncLockKp | then_async_kp_get() |
Chain to async lock |
| Kp | LockKp | then_lock_kp_get() |
Chain to sync lock |
| Kp | AsyncLockKp | then_async_kp_get() |
Chain to async lock |
Each chain operation requires explicitly calling a method with the next keypath. This makes the navigation path clear and the lock acquisitions visible.
- Methods that cross into async territory return
async fnand require.await - Methods staying in sync world remain synchronous
All chaining methods use lifetime parameters ('a) to ensure the keypaths and their closures live long enough for the operation.
Unlike the then() and compose() methods on individual keypath types that return new composed keypaths, the interoperability methods directly execute the navigation and return the value. This avoids complex lifetime and Clone bound issues with closures.
All interoperability operations maintain the shallow cloning guarantee:
Arcclones only increment reference counts- Lock accessor structs (
TokioMutexAccess, etc.) contain onlyPhantomData(zero-cost) - Function pointers are cheap to copy
- No deep data cloning occurs
- Sync locks block the current thread
- Async locks yield to the executor (non-blocking)
- Chaining multiple locks acquires them sequentially (as needed by the navigation path)
The interoperability is tested with:
test_async_kp_then(async_lock.rs): AsyncLockKp -> Kptest_kp_then_lock_kp(lib.rs): Kp -> LockKptest_kp_then_async_kp(lib.rs): Kp -> AsyncLockKptest_full_chain_kp_to_lock_to_async(lib.rs): Kp -> LockKp -> AsyncLockKp (full chain)test_lock_kp_then_async_kp(lock.rs): LockKp -> AsyncLockKp
Total: 82 tests passing (including all interoperability tests)
Due to Rust's type system, you often need to provide type annotations when creating the intermediate keypaths:
let value_kp: KpType<Inner, i32> = Kp::new(
|i: &Inner| Some(&i.value),
|i: &mut Inner| Some(&mut i.value),
);Once you enter async territory (via AsyncLockKp or then_async_kp_get()), all subsequent operations must be async and require .await.
The AsyncLockKp struct requires Clone bounds on its function types (G1, S1, etc.), which means closures that capture non-Clone data cannot be used. Use function pointers or ensure captured data is Clone.
Potential improvements for interoperability:
-
Builder Pattern: Fluent API for chaining
ChainBuilder::new() .with_kp(regular_kp) .with_lock_kp(lock_kp) .with_async_kp(async_kp) .execute(&root) .await
-
Macro Support: Generate interoperability chains from a declarative syntax
chain!(root => field => sync_lock => async_lock => value)
-
Composed Return Types: Return new composed keypaths instead of directly executing (Currently challenging due to closure lifetime and
Cloneconstraints)
The interoperability feature enables seamless navigation through complex data structures with mixed synchronization strategies, maintaining type safety, shallow cloning guarantees, and explicit lock acquisition points throughout the navigation path.