Context
I have a DataStore<Key,Value>
trait that abstracts out data storage. (For example, I can create a simple implementation of this trait for data stores that wrap Vecs
and HashMaps
.) I would like this abstraction because some use cases/targets require small but computationally inefficient stores and others allow for larger stores. (Edit: added references to self
in trait definition, below.)
// Stores data of type V indexed by K
trait DataStore<K, V> {
fn new() -> Self;
fn get(&self, k: K) -> Option<&V>;
fn insert(&mut self, v: V) -> Option<K>;
}
I now want to define a struct Thing
that contains two data stores: a store on one type, Apple<T>
, and a store on another type, Banana<T>
. Here is my first attempt,
// some objects I'd like to keep in DataStores
struct Apple<T> { shine: T }
struct Banana<T> { spottedness: T }
// Attempt #1: cumbersome, have to always specify generic constraints when
// using Thing elsewhere
pub struct Thing<K, T, AppleStore, BananaStore>
where AppleStore: DataStore<K, Apple<T>>,
BananaStore: DataStore<K, Banana<T>>
{
apple_store: AppleStore,
banana_store: BananaStore,
}
This approach is cumbersome to work with since I have to always type out <K, T, AppleStore, BananaStore> where ...
whenever I want to pass Thing
to a function or implement a trait for Thing
even if said function or trait doesn't care about either of the two stores. For example, if I want to implement a trait for Thing
that does some unrelated operations to other attributes with type T
I still have to tell it about K
, AppleStore
, and BananaStore
.
I learned about type aliases and tried the following:
// Attempt #2: looks easier to use. only two generics on Thing: the type of
// the indexes and the type of the internal parameters. not sure
// about the role of dyn, though, since this should be checkable
// at compile time
type AppleStore<K, T> = dyn DataStore<K, Apple<T>>;
type BananaStore<K, T> = dyn DataStore<K, Banana<T>>;
pub struct Thing<K, T> {
apple_store: AppleStore<K, T>,
banana_store: BananaStore<K, T>,
}
A new problem appears when I try to create a new BananaStore
in Thing
's constructor. This is allowed in Attempt #1 since traits are allowed to implement functions that (1) do not take &self
as argument and (2) return type Self
. But this is not allowed in Attempt #2 because dynamic traits need things to be Size
d and that's not allowed with Self
returns. (Or something?)
impl<K, T> Thing<K, T> {
pub fn new(apple_store: AppleStore<K, T>) {
Thing {
apple_store: apple_store,
banana_store: BananaStore::new() // not allowed to do this with
// dynamic type aliases?
}
}
Question
Do I need to create a BananaStore
outside of Thing
and pass it in as a parameter or is there a way to hide the construction of BananaStore
from the outside? I suppose something like a ThingBuilder
may be a valid approach if one of my goals is to hide unnecessary (optional) object creation. But I also don't want to provide a default implementer of BananaStore
: the user should explicitly declare what kind of DataStore
is used for BananaStore
.
I formulate the problem in this way because eventually I want Thing
's AppleStore
to actually be shared among multiple Thing
instances; that is, multiple Things
can reference the same Apple<T>
in a store. But each Thing
will have it's own BananaStore
. I know this will require using Rc
or Arc
or something like that on AppleStore
but I will cross that bridge when I get to it.
Thing
? Doing so is often a bad idea, especially if you have a lot of code that does not depend on those bounds; it just constrains code that has no need for the constraints. – trentcl