I have the following situation: I have two traits, lets call them T1 and T2. For T2 I have many implementations and I want users of my library to also be able to add implementations to it.
For T1 all implementations are generic over various sub-traits of T2 so something like
Now what I want in the end is that a potential application can select at runtime an implementation of T1 by selecting an implementation of T2 and selecting a generic struct such that the generic struct with the selected T2 implementation implements T1.
Now I want this system to be extendable, which means that users can:
Add new T2 implementations that implement various sub-traits
Add new Subtraits of T2
Add new generic structs that implement T1 and the generic argument is bound by one of the subtraits of T2.
How can I approach this? Is working with traits even the right thing (given that they are purely compile-time while I want the two "algorithms" to be selectable at runtime). On the other hand I want to avoid having to implement logic for any pair of algorithm, I want to use the power of trait abstractions to group together T2 implementations and their behaviors.
Can you describe what you're trying to do in somewhat less abstract terms? Some of what you describe is definitely possible with trait objects, but it's unlikely that you'll get to something fully-flexible and extensible without major ergonomic tradeoffs.
In particular, the fact that not every implementation of T2 will satisfy the requirements of the various generic structs could be painful to work around: Rust doesn't have the sort of introspection built-in to ask what other traits an object implements that weren't already required, so you'll need to build that machinery yourself— That's tricky even at compile time, and adding runtime dispatching will make it even more complicated.
The specifics of how and when you register the various T1 and T2 options as available, how the subtraits are defined and used, how the generic structs are used in practice, and lots of other little details will really make the difference in what a reasonable approach looks like and how complex it will have to be.
I am currently designing an extendable library that can be used to organize sport competitions. I want to it to be flexible to work with as many sports as possible. There are many modular parts of such a system that can be different by sport. For example a "match" between two participants can have many different outcome metrics (for example if it consists of multiple games it can have games won metrics, if the sport has goals one can think about goal difference, ...). Then a competition needs a ranking (a way to sort participants). This ranking might depend on various metrics of the match outcomes (it might depend on goal difference for example). So that means that a ranking needs to be compatible with a match type.
So I want to have different implementations of Match and different implementations of Ranking and they can be compatible or not. In my design I would make the Ranking implementations generic over the Match (or MatchOutcome) implementations.
So now the user of this library should be able to add new Match types as well as new Ranking types. It should also be able to say with which Match types a Ranking type is compatible (sub-traits have the advantage that they would make things like goal_difference() available on a type level). Then the user should be able to have a runtime "settings" dialogue such that that at runtime one can select which match type and which ranking type one wants to use for a competition.
At runtime, information about what traits are implemented by what has already been erased. You'll have to duplicate it to get from Match to Ranking implementors, and sprinkle it all with std::any::Any, and that will turn out unwieldy.
Rust executable reads stdin as JSON, then process it and return another JSON in stdout. The main module selects Rust executable by name matching a sport name.
Pretty match as (1) but instead of a direst launch an executable it makes HTTP request.
Obviously the JSON represents your trait interface. It was easier in Java, because I could use dynamically loaded classes. But Rust approach isn't bad either.