Trait function with a `Self` parameter prevents object construction

Let there be a trait PharmaChemical, representing a pharmaceutical chemical.

pub trait PharmaChemical{
/// Other less interesting routine functions 
fn  affinity(&self, other : &Self) -> f64;

The function affinity basically allows one to compute a scalar between two structs of the same type implementing PharmaChemical. I would like this affinity function to remain.

HydrogenoidChemical, OxygenoidChemical, NitrogenoidChemical etc can be assumed to be structs which implement PharmaChemical.

My work requires me to implement another trait ChemicalPool -- representing a collection of chemicals for analysis. An example is below :

pub trait ChemicalPool{
    /// Returns the name of the pool.
    fn name(&self) -> &str;
   /// Should return the chemicals in the pool
   fn chemicals(&self) -> &[Box<dyn PharmaChemical>];

Now, I get the error which says that dyn PharmaChemical cannot be made into an object. For a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically
The compiler further says that if I move the affinity function out the trait the error can be handled. The affinity function is the problem because it has a Self parameter.

During run-time, a pool can contain many different types of chemicals. And we compute affinity for every pair of chemicals. If the two chemicals are not of the same type ( e.g :- HydrogenoidChemical ), it would result in a panic!. This is infact one of the ways of initial-phase testing to remove unnecessary chemicals from the pool. The chemicals function is often needed to get a slice of chemicals in the pool.

Now I come from a C++ background and so I ask : Am I modelling the problem wrong that does not conform to the Rust way of thinking ? How can I continue using a function like affinity while, keeping a dynamic collection of objects which I can return as a slice ?

This is going to be a bit vague because I don't know your situation, but a couple of considerations:

  • Do you have a need to use dynamic trait objects besides convenience? The fundamental issue is a system where you want to erase types but also have type-dependent operations at the same time.
  • Depending on the former, and how the actual chemical structs look (and how many they are, with apologies to my chemistry teacher), I'd consider using enums instead of dynamic types for modelling your chemical "topology" first, since it should be a set of closed states.
  • I'd also go for not using panic! in that way, as a late-stage enforcement deep on the inside of a component like that. I'd try and find an API that produces a Result<f64, AffinityError> or something like that, and then use an expect at the actual point where it is obvious that there should be no wrong chemical pairings in the process.

The approach you're currently following (with erased types) is one I'd find myself in when I'd expect people outside of the project to provide the chemical definitions with "shapes" and properties I can't predict.

I am working on a library. While structs for many pre-defined chemical classes can be defined, we also need to provide the flexibility to define new structs which cannot be predicted well in advance. Enums I consider will be useful when the class of chemicals is fixed or when every change can be reflected in the library.

The reason I use a dyn trait object is because I cannot use impl PharmaChemical into the return type of trait functions. I don't have enough understanding of Rust to reason why it should or shouldn't be allowed.

The main tide you're swimming against is wanting to type-erase PharmaChemical implementors even though affinity only makes sense when the (base) types are the same. [1] If you want to push forward with that anyway, you'll need to work "upcast to dyn Any then downcast to concrete type" into your design.

Here's a basic example.

It has downsides; you probably won't get the behavior you want if you implement PharmaChemical for Box<dyn PharmaChemical>, due to the blanket implementation. But I don't recommend doing that in this case anyway, because then you'd have to use panic! in affinity, and I agree with @phaylon that you should do something non-inherently-panicking (try_affinity) instead.

It's not implemented yet but wouldn't help you here anyway. Return position impl Trait (RPIT) is an opaque alias over a concrete type; it doesn't truly type erase like dyn Trait. So everything in the returned slice would need to be the same (base) type.

You should be aware that Rust has no trait-based subtyping.[2] You can emulate it to some extent with dyn Any and its downcasting, but as you can see, it's a somewhat manual process. You'll lose a lot of strongly-typed (compile-time enforced) benefits and end up with logic-error prone paths in your program, like your "compare everything in the slice at runtime and panic if the implementor messed up".

And you're never going to have two distinct chemical structs that have a sub/super type relationship.

It took me a minute to understand how that makes any sense at all. It sounds like there's a need to sort by base type, so one can return contiguous slices of the same base type from a Vec or such. You could look into a "typemap" library instead. [3] But a better idea is probably to split up the "I need the ability to hold many different chemicals" and the "I'm operating on a pool of one particular chemical" cases. When everything needs to be the same type, you should be utilizing the type system to enforce that.

I'm getting the feel that, unless things are redesigned in a more comprehensive way, ChemicalPool makes more sense as a struct than a trait. Unless you're providing every implementor of the trait perhaps. I'm getting this feel because anyone implementing it is going to need to know the base types of all their chemicals behind the scene (or use their own typemap etc) in order to do the correct (sorting and) selection for fn chemicals (if nothing else). [4]

Either they're also downcasting all over the place all the time (very unidiomatic) or they have their strongly typed chemicals which they eventually have to type-erase into a box and shove in Vec. (And if they mess that up a bit... runtime panic.)

Having to implement that myself is nothing I would want out of a library, anyway.

  1. Your ChemicalPool is also basically trying to enforce some type of field inheritance pattern. ↩︎

  2. And no struct-based subtyping either. ↩︎

  3. I'm not sure which one to recommend. ↩︎

  4. I'm getting the feel to a lesser extent from the methods that mandate what type of fields implementors have. ↩︎


This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.