I'm working on writing a toy database implementation in Rust and I can't get around this trait requirement to be object safe. I basically want a way of representing a database table with columns of different types in rust. This is what my current existing code kind of looks like.
trait Primitive {}
impl Primitive for i32 {}
struct Table {
value: HashMap<String, Arc<dyn ColType>>,
}
trait ColType {
fn get_data(&self) -> &Vec<impl Primitive>;
}
struct TableValue<T: Primitive> {
data: Vec<T>
}
impl <T: Primitive> ColType for TableValue<T> {
fn get_data(&self) -> &Vec<T> {
return &self.data;
}
}
This is a simplified version of how far I have gotten on the data storage side. That being said this code gives the warning ColType
cannot be made into an object. Basically I just want this or some similair solution to compile. The requirements are:
I need Primitive
to be a trait (implemented on i32
's and such). This is because I don't want to use an enum because I'm not sure how I would ensure each value is the same variant and also overhead things.
I don't want to do any Vec<Box<impl Primitive>>
stuff because I believe there should be a way to not do something that awful. (why am I making a new pointer per item when a Vec<T> where T: Primitive
works!)
The solution must actually be able to store things with (I got something close to working with HashMap<String, Arc<dyn ColType<dyn Primitive>>>
by adding a generic argument to ColType<T: Primitive>
but this ended up making trait bounds that were impossible to be satisfied.
In my head I should be able to return some heap allocated vec of type Vec<impl ColType + Sized>
but that might just be me.
The problem with returning a impl Trait
from a method is that the exact type is only determined by the implementors, not the trait. In other words, to know what the concrete return type is, we need to know the concrete type of the implementor. But being able to know the concrete return type is exactly the thing a trait object opts out of. So there cannot be a trait object which would have to be agnostic of the concrete type returned.
You can manually factor out the return type into an associated type and thus make it possible for users to declare that all the trait objects return the same type:
use std::sync::Arc;
use std::collections::HashMap;
trait Primitive {}
impl Primative for i32 {}
struct Table<T: Primitive> {
value: HashMap<String, Arc<dyn ColType<Item = T>>>,
}
trait ColType {
type Item: Primitive;
fn get_data(&self) -> &[Self::Item];
}
struct TableValue<T: Primitive> {
data: Vec<T>
}
impl <T: Primitive> ColType for TableValue<T> {
type Item = T;
fn get_data(&self) -> &[T] {
&self.data
}
}
I also switched to returning slices, unless you really need the capacity
it's less limiting than returning &Vec<T>
.
If you need to be able to store different types in Table
you'll have to create a type erased version of ColType
and use that:
struct Table {
value: HashMap<String, Arc<dyn ColTypeErased>>,
}
trait ColTypeErased {
fn get_data(&self) -> Vec<&dyn Primitive>;
}
impl<T: ColType> ColTypeErased for T {
fn get_data(&self) -> Vec<&dyn Primitive> {
ColType::get_data(self).iter().map(|x| x as &dyn Primative).collect()
}
}
This does require an allocation though.