As a learning/play/hobby project, I'm making a small application where I attempt to decouple the data access from the rest of the application as much as possible. This way, I should later be able to experiment with other data managing frameworks or methods (database, on file, in memory; ORM vs fixed queries etc etc) with little impact to other (equally experimental) areas of this project.
In short, I'm trying to keep everything as generic as possible.
EDIT: The below examples aren't very clear. See the third post for playground examples.
Right now, I have a generic DataStruct
, looking somewhat like this (cut a bit to reduce noise in this question):
pub trait DataStruct
where Self: Sized {
type DataProvider;
/// Gets all persisted structs from a data store.
fn get_all(data_provider: &Self::DataProvider) -> Vec<Self>;
}
The idea behind the DataProvider
associated type and passing it to the functions, is to avoid global state. I can make a DataProvider
somewhere in the application, and pass it around.
In another module, for a specific data implementation, I have e.g. for the fictitious BazDB:
pub trait BazDbDataStruct: DataStruct {
fn table_name() -> String;
fn query_all(data_provider: &BazDbDataProvider) -> Vec<Self> {
let conn = data_provider.open_baz_db_connection();
conn.find_all_the_things_for_table(Self::table_name())
}
}
Now, let's see how this works for a persistable struct Foo
:
struct Foo {
id: i64,
value: String
}
impl BazDbDataStruct for Foo {
fn table_name() -> String {
"FOO".to_string()
}
}
impl DataStruct for Foo {
type DataProvider = BazDbDataProvider;
fn get_all(data_provider: &BazDbDataProvider) -> Vec<Self> {
Self::query_all(data_provider)
}
}
An important part of this question is the signature of this get_all
implementation. I have tested this in a more complete example, and Rust seems to accept this without problems.
The problem comes when I now have a second struct corresponding to a table in my BazDB. The implementation for Bar
would look as follows:
struct Bar{
id: i64,
other_value: String
}
impl BazDbDataStruct for Bar{
fn table_name() -> String {
"BAR".to_string()
}
}
impl DataStruct for Bar{
type DataProvider = BazDbDataProvider;
fn get_all(data_provider: &BazDbDataProvider) -> Vec<Self> {
Self::query_all(data_provider)
}
}
So while the implementation for the concrete BazDbDataStruct
is (obviously) different, the implementation for the generic DataStruct
is (equally obviously) identical to that of Foo
. Still, this all runs and tests fine.
However, I now try to genericize the DataStruct
implementation to avoid duplication and boilerplate:
impl<T> DataStruct for T
where T: BazDbDataStruct {
type DataProvider = BazDbDataProvider;
fn get_all(data_provider: &BazDbDataProvider) -> Vec<Self> {
T::query_all(data_provider)
}
}
This implementation is identical to the above two implementations, but Rust suddenly doesn't like the method signature for get_all anymore:
error[E0053]: method `get_all` has an incompatible type for trait
--> src\data\core\bazdb.rs:763:35
|
763 | fn get_all(data_provider: &BazDbDataProvider) -> Vec<Self> {
| ^^^^^^^^^^^^^^^^^^^ expected associated type, found struct `data::core::bazdb::BazDbDataProvider`
|
::: src\data\core\mod.rs:46:31
|
46 | fn get_all(data_provider: &Self::DataProvider) -> Vec<Self>;
| ------------------- type in trait
|
= note: expected type `fn(&<T as data::core::DataStruct>::DataProvider) -> std::vec::Vec<T>`
found type `fn(&data::core::bazdb::BazDbDataProvider) -> std::vec::Vec<T>`
So I sort of understand this error, but I also kinda don't Why is this a problem with this generic implementation, yet Rust had no problems when the two identical previous implementations (which had the same function signature). I wouldn't be suprised if generics threw some spanners in the wheels somewhere, but I don't know why, nor what to do about it. The
DataProvider
type does not depend on the generic T
, it is defined in this impl. So why does Rust insist on needing &<T as data::core::DataStruct>::DataProvider
here, yet was perfectly fine with the &BazDbDataProvider