I've been making a big project and finally end up doing stuff that's kinda similar to abstract classes in Python.
Step 1: Data Structures
In the beginning I had several tables of data like this:
struct Table1 {
map: HashMap<Key1, Row1>,
extra_index: HashMap<Category1, Vec<Key1>>
}
struct Table2 {
... // similar as in Table1, but with different types
}
I needed 4 such tables, with different types, which allows keeping natural keys and natural data, which is simpler to reason about and debug, rather than refined and abstract common data.
A hard requirement is that in one table, rows refer to one or two rows in another table, which complicates things and requires some accessors to raw data.
Step 2: Traits
I wanted to make common interfaces and created traits. They all were fine, except every TableX
returned different types, so I needed associated types. And the rows they were returning, also had custom properties, hence another trait with another associated type. I ended up with 3 traits, each with associated type.
trait Tbl {
type Row: Row;
type ReferencedTable: Tbl;
// some tables have none, so I had to impl Tbl for ()
fn query(key: <<Self as Tbl>::Row as Row>::Key) -> Row;
// aaargh, cannibal turbofish!
}
trait Row {
type Key: Hash, blah blah blah;
...
}
Step 3: Putting Things Together
After I spent a month introducing the traits, refactoring and adjusting the code, I thought I wanted a common web API to make queries like "run an algorithm on table N for rows category M, params X, Y, Z".
Must be easy, doesn't it? Just Box-dyn the trait, and you're fine?
The Gotcha
Well, no, because Box<dyn Trait>
returns associated types that come not boxed, and must be stone-hard specified.
Turns out, a trait has an associated type works similar to generic nested type, like MyType<X>
can't be put in a vec with MyType<Y>
, same way impl SomeType<AssocType=X>
& impl SomeType<AssocType=Y>
are different and can't be put together.
What I Did Wrong
Again, I see it could have been better had I start with writing the client code first, which shapes the internals, not the other way around. This is what I'm doing with a blank small crate now.
Enum like Abstract Class
Right now, I have to make a wrapper class that converts things like query data in common type
=> table X inner type
=> table X mapping
=> common algorithm data type
=> algorithm result
=> table X inner type
=> common output type
.
A serious difficulty is that I must keep track of these inner-outer types, like ying-yang and separate them carefully, and the conflicts between them sometimes get uncovered only when you remove todo!()
s and try implementing at least the wrappers.
The fact that the wrapper must process only the common types, means that concrete TableX
classes must implement conversion themselves, hence there either will be copy&pasted code, or shared code by using traits & provided methods, which implies... associated types... again!
So, worries:
- either a good deal of copy&pasted code.
- or another gotcha with code in provided methods
- the wrapper starts to look like abstract class...
- or like God object
Any comments, advices?