eletvon
eletvon

Reputation: 45

Refine trait generic parameter

Given this simplified code (which doesn't compile), how I can inform the compiler that, that in the impl I only wish fn execute to allow cases where Model::Parameters == P?

trait Model {
    type Parameters;
    fn run(&self, p: &Self::Parameters) -> f64;
}

trait Analysis {
    fn execute<M: Model>(&self, model: M) -> (M::Parameters, f64);
}

pub struct Data<P>{
    pub value: P, 
}

impl<P> Analysis for Data<P> {
    fn execute<M: Model>(&self, model: M) -> (M::Parameters, f64) {
        let p = self.value;
        (p, model.run(&p))
    }
}

I tried a where clause:

fn execute<M>(&self, model: M) -> (M::Parameters, f64) 
where M: Model<Parameters = P> {
    let p = self.value;
    (p, model.run(&p))
}

but the compiler rejects it as that deviates from the declaration in the Analysis trait, which knows nothing about P.

Upvotes: 0

Views: 60

Answers (1)

Kevin Reid
Kevin Reid

Reputation: 43842

You can't add bounds to a trait method, because that would break the guarantee of Rust's trait system, that generic code will be usable in every case where its bounds are met — “no post-monomorphization errors”. You can add arbitrary bounds to a trait implementation, but that doesn't help here since P only appears in relation to M which is not a parameter or associated type of the trait, only its method. The trait's current signature is forcing every implementation to work for all possible Models.

If you want to allow an Analysis to only work for certain kinds of Parameters, then you have to put that in the trait. One simple possibility is that Analysis should have an associated type like Model does, but I'm guessing that you would have thought about it already. Another possibility is to give Analysis a type parameter, which means that each implementation can choose whether or not it works for different Parameters. This compiles:

trait Model {
    type Parameters;
    fn run(&self, p: &Self::Parameters) -> f64;
}

trait Analysis<P> {
    fn execute<M: Model<Parameters = P>>(&self, model: M) -> (P, f64);
}

pub struct Data<P> {
    pub value: P, 
}

impl<P: Clone> Analysis<P> for Data<P> {
    fn execute<M: Model<Parameters = P>>(&self, model: M) -> (P, f64) {
        let p = &self.value;
        (p.clone(), model.run(p))
    }
}

// can also write impl<P> Analysis<P> for AnalysisThatDoesntCare {}

(I also had to change the function body to clone self.value, since execute returns P by value.)

Or maybe the Analysis trait should include the Model instead, so the trait is generic but the function isn't:

trait Model {
    type Parameters;
    fn run(&self, p: &Self::Parameters) -> f64;
}

trait Analysis<M: Model> {
    fn execute(&self, model: M) -> (M::Parameters, f64);
}

pub struct Data<P> {
    pub value: P, 
}

impl<M, P> Analysis<M> for Data<P>
where
    M: Model<Parameters = P>,
    P: Clone,
{
    fn execute(&self, model: M) -> (P, f64) {
        let p = &self.value;
        (p.clone(), model.run(p))
    }
}

This version allows the implementations to constrain what Models they work with, not just their Parameters type, and it might be simpler when working with non-generic implementors of Analysis.


(P.S. Rust API style is to name traits after the operations they enable, not a description of the value, when that's practical. So, possibly your trait should be named Analyze with method analyze() (or analyse()) instead of Analysis and execute().)

Upvotes: 2

Related Questions