Problem
The current ZipperValues trait is defined roughly as:
pub trait ZipperValues<V> {
fn val(&self) -> Option<&V>;
fn val_at<K: AsRef<[u8]>>(&self, path: K) -> Option<&V>;
}
This works well for zippers backed by a concrete trie, where values already exist in storage and both methods can return references into that storage.
However, this becomes problematic for computed or algebraic zippers, such as a virtual SubtractZipper<A, B> representing A - B.
At the current focus, SubtractZipper can cache the computed value internally, so implementing:
fn val(&self) -> Option<&V>
is straightforward.
For val_at, however, subtraction may need to construct a new value:
match lhs_val.psubtract(&rhs_val) {
AlgebraicResult::None => None,
AlgebraicResult::Identity(...) => lhs_val,
AlgebraicResult::Element(v) => ??,
}
In the Element(v) case, v is newly created by the operation. It cannot be returned as &V, because it is only a temporary local value.
This means the current val_at(&self) -> Option<&V> contract assumes that every value queried through a zipper already has stable backing storage, which is not true for virtual/computed zippers.
Possible solutions
Option 1: Return Cow<'_, V> from val_at
Change the API to something like:
fn val_at<K: AsRef<[u8]>>(
&self,
path: K,
) -> Option<Cow<'_, V>>
where
V: Clone;
This naturally represents both cases:
when the result already exists in the underlying trie, and:
when an algebraic zipper needs to synthesize a new value.
For example, subtraction could implement:
match lhs_val.psubtract(&rhs_val) {
AlgebraicResult::None => None,
AlgebraicResult::Identity(mask) if mask == SELF_IDENT => {
lhs_val.map(Cow::Borrowed)
}
AlgebraicResult::Element(v) => {
v.map(Cow::Owned)
}
_ => unreachable!(),
}
Advantages
val_at works uniformly for concrete and computed zippers.
- The return type accurately describes the semantics: a value may either be borrowed or synthesized.
- Callers can usually treat
Cow<V> similarly to &V via Deref.
- Computed zippers do not need artificial caches or interior mutability.
Disadvantages
- Changes the existing API for all
ZipperValues implementations and callers.
- Introduces
Cow into a relatively low-level zipper interface.
- May require a
V: Clone bound, depending on how the API is structured.
- Callers that only deal with concrete zippers now receive a more general type than they actually need.
Option 2: Split focused-value and arbitrary-path value capabilities
The issue also suggests that val() and val_at() may represent two fundamentally different capabilities.
val() queries the zipper's current focus. A virtual zipper can maintain/cache the value at its focus.
val_at() queries an arbitrary descendant without moving the zipper. A computed zipper may need to synthesize that result dynamically.
We could therefore split the trait:
pub trait ZipperValue<V> {
fn val(&self) -> Option<&V>;
}
pub trait ZipperValues<V>: ZipperValue<V> {
fn val_at<K: AsRef<[u8]>>(
&self,
path: K,
) -> Option<&V>;
}
Concrete trie zippers could continue implementing ZipperValues<V> exactly as they do today.
Computed zippers such as SubtractZipper could implement only:
unless they can provide stable references for arbitrary paths.
If arbitrary-path access to computed values is needed, we could add a separate capability, for example:
pub enum ValueRef<'a, V> {
Borrowed(&'a V),
Owned(V),
}
pub trait ZipperComputedValues<V> {
fn val_at<K: AsRef<[u8]>>(
&self,
path: K,
) -> Option<ValueRef<'_, V>>;
}
or use Cow<'_, V> there instead.
Advantages
- Preserves the existing zero-copy
Option<&V> API for concrete zippers.
- Makes the capability distinction explicit.
- Computed zippers do not have to implement an operation they cannot naturally support.
- Avoids imposing
Cow or ownership semantics on all zipper users.
Disadvantages
- Adds another trait/capability to the zipper API.
- Generic code that currently requires
ZipperValues<V> may need to reconsider whether it actually needs val_at.
- Code that needs arbitrary-path access across both concrete and computed zippers may still require another abstraction.
Why not cache values returned by val_at?
A SubtractZipper could theoretically cache synthesized values by path and then return references into that cache.
However, this seems undesirable:
val_at(&self) would require interior mutability.
- A normal
RefCell/RwLock cache cannot trivially return a plain &V after releasing its guard.
- Stable-reference storage would require something closer to an arena or append-only allocation scheme.
- The cache could grow with every queried path.
- This adds significant complexity merely to preserve an API contract that does not naturally fit computed zippers.
Therefore, caching arbitrary val_at results does not appear to be a good general solution.
Question
Which API direction do we prefer?
-
Generalize val_at to return Cow<'_, V>, allowing both borrowed and synthesized values through one interface.
-
Split the trait, keeping val() -> Option<&V> as the basic capability and reserving the current reference-returning val_at() for zippers backed by stable value storage.
A hybrid is also possible: split the traits first, then provide a separate computed-value val_at capability returning Cow<'_, V> or a custom Borrowed | Owned enum.
The decision mainly depends on whether we consider arbitrary-path value lookup an essential operation that should work uniformly across all zipper types, or an optional capability of zippers with suitable backing storage.
Problem
The current
ZipperValuestrait is defined roughly as:This works well for zippers backed by a concrete trie, where values already exist in storage and both methods can return references into that storage.
However, this becomes problematic for computed or algebraic zippers, such as a virtual
SubtractZipper<A, B>representingA - B.At the current focus,
SubtractZippercan cache the computed value internally, so implementing:is straightforward.
For
val_at, however, subtraction may need to construct a new value:In the
Element(v)case,vis newly created by the operation. It cannot be returned as&V, because it is only a temporary local value.This means the current
val_at(&self) -> Option<&V>contract assumes that every value queried through a zipper already has stable backing storage, which is not true for virtual/computed zippers.Possible solutions
Option 1: Return
Cow<'_, V>fromval_atChange the API to something like:
This naturally represents both cases:
when the result already exists in the underlying trie, and:
when an algebraic zipper needs to synthesize a new value.
For example, subtraction could implement:
Advantages
val_atworks uniformly for concrete and computed zippers.Cow<V>similarly to&VviaDeref.Disadvantages
ZipperValuesimplementations and callers.Cowinto a relatively low-level zipper interface.V: Clonebound, depending on how the API is structured.Option 2: Split focused-value and arbitrary-path value capabilities
The issue also suggests that
val()andval_at()may represent two fundamentally different capabilities.val()queries the zipper's current focus. A virtual zipper can maintain/cache the value at its focus.val_at()queries an arbitrary descendant without moving the zipper. A computed zipper may need to synthesize that result dynamically.We could therefore split the trait:
Concrete trie zippers could continue implementing
ZipperValues<V>exactly as they do today.Computed zippers such as
SubtractZippercould implement only:unless they can provide stable references for arbitrary paths.
If arbitrary-path access to computed values is needed, we could add a separate capability, for example:
or use
Cow<'_, V>there instead.Advantages
Option<&V>API for concrete zippers.Cowor ownership semantics on all zipper users.Disadvantages
ZipperValues<V>may need to reconsider whether it actually needsval_at.Why not cache values returned by
val_at?A
SubtractZippercould theoretically cache synthesized values by path and then return references into that cache.However, this seems undesirable:
val_at(&self)would require interior mutability.RefCell/RwLockcache cannot trivially return a plain&Vafter releasing its guard.Therefore, caching arbitrary
val_atresults does not appear to be a good general solution.Question
Which API direction do we prefer?
Generalize
val_atto returnCow<'_, V>, allowing both borrowed and synthesized values through one interface.Split the trait, keeping
val() -> Option<&V>as the basic capability and reserving the current reference-returningval_at()for zippers backed by stable value storage.A hybrid is also possible: split the traits first, then provide a separate computed-value
val_atcapability returningCow<'_, V>or a customBorrowed | Ownedenum.The decision mainly depends on whether we consider arbitrary-path value lookup an essential operation that should work uniformly across all zipper types, or an optional capability of zippers with suitable backing storage.