I have a project I'm working on called columnar
that lays data out differently than Rust does. It works great, specifically for concrete types, but I'm struggling to put some structure on the traits in order to make it more useful in generic contexts.
Let's take as an example pairs (A, B)
. If you wanted to store many of these, you could use a Vec<(A, B)>
. The thesis behind columnar
is that you might actually prefer (Vec<A>, Vec<B>)
; that is, you might prefer to store the tuple elements in separate containers. There are a bunch of reasons, and columnar
handles more than just pairs of things. The link above explains more, or you can suspend disbelief for the moment.
The crate design is based around several traits, but the problem distills down to just a few, which I've simplified past the point of being useful (in case you reached a conclusion about that by looking at them).
trait Index {
type IndexRef;
fn get(&self, index: usize) -> Self::IndexRef;
}
trait Borrow {
type Borrowed<'a>;
}
trait Columnar {
type Ref<'a>;
type Container where for<'a> &'a Self::Container: Index<IndexRef = Self::Ref<'a>>,
for<'a> <Self::Container as Borrow>::Borrowed<'a>: Index<IndexRef = Self::Ref<'a>>,
}
The intent is that implementors of Columnar
, for example (A, B)
, can name reference types, like (&'a A, &'a B)
, as well as a container that can provide these references. The container type, say (Vec<A>, Vec<B>)
, also comes in a read-only Borrowed<'a>
form, for example (&'a [A], &'a [B])
, that you might get from a non-(Vec<A>, Vec<B>)
source. Both of the containers should implement Index
with a reference type that matches Columnar::Ref<'a>
. This last point is the sticking point: we want this so that folks have any structure at all about the reference types, short of having concrete implementors in hand, and the ability to see what the concrete reference types are.
At this point, I should say that the code above does not work. Rust does not like the second for<'a>
constraint, because it does not see a 'a
in the type on the left hand side of the :
constraint. I don't wholly understand why that is, but smart people I know believe that it makes some sense. Informally, the explanation was that the Borrowed<'a>
types may not actually depend on 'a
, in contrast to how an &'a
type certainly does depend on 'a
.
So, I'm hoping to brainstorm a bit on alternate ways to describe what feels like a not-wildly-complicated relationship between (A, B)
, (Vec<A>, Vec<B>)
, (&'a [A], &'a [B])
, and (&'a A, &'a B)
. I have types, owning container types that can be written at, and a class of lifetimed read-only container types that can be read from. I'd love to communicate that when reading from them, you get a consistent type back.
I've gone through a few alternate designs, though they seem to have their own limitations. For example, IndexRef
is intentionally not a GAT because it wants to reflect the lifetime of its implementor, rather than to vary with the lifetime of &self
when get
is called. It seems like adding a lifetime to Index
, i.e. a trait Index<'a>
might work, but scares me that one will only be able to combine identically lifetimed containers (and GATs are invariant, making aligning the lifetimes challenging).
Concrete questions include:
- Are there better patterns for expressing the constraints among the various types, containers, and references?
- Is that second constraint fundamentally problematic, or only problematic to Rust's current solver?
I'm happy to unpack more about the design and constraints, if folks would find that helpful. There are other usability / extensibility constraints that might rule out some solutions that would otherwise work.