Idiomatic way to implement interchanged trait bound

Viewed 44

I have a problem with how to force a trait bound on a struct. The struct will not make sense without the trait and therefore I want to enforce that user-defined model implement PricingModel. The issue arises because the trait also takes the Options struct as an argument to compute values. It could be avoided by passing fields individually but this seems like a hacky solution.

The Options struct holds a Model struct which has to implement PricingModel. PricingModel however needs to take a reference to Options as an argument.

The below is something of a condensed example:

struct Model<M: PricingModel<M>>;

trait PricingModel<M: PricingModel<M>> {
    fn get_price(&self, opt: &Options<M>) -> f64;
}

impl<M: PricingModel<M>> PricingModel<M> for Model<M> {
    fn get_price(&self, opt: &Options<M>) -> f64 {
        opt.price + 1.0
    }
}

struct Options<M: PricingModel<M>> {
    price: f64,
    model: M,
}

impl<M: PricingModel<M>> Options<M> {
    fn new(price: f64, model: Model<M>) -> Self {
        Options {
            price,
            model
        }
    }
}

fn main() {
    let model = Model;
    let opts = Options::new(1.0, model);
    opts.model.get_price()
}

This particular code only generates the error...

error[E0392]: parameter `M` is never used                                                                                                                                                                                              
 --> src\main.rs:1:14
  |
1 | struct Model<M: PricingModel<M>>;
  |              ^ unused parameter
  |
  = help: consider removing `M`, referring to it in a field, or using a marker such as `PhantomData`

...but is not the central part of the question.

I have not been able to figure out the idiomatic way to express this dependency that doesn't break the compiler (the last attempt was a circular reference for the trait bounds).

This seems like a rather simple problem, but the answer eludes me. Thoughts?

1 Answers

The Options struct holds a Model struct which has to implement PricingModel. PricingModel however needs to take a reference to Options as an argument.

I think what you are looking for is not how to couple a trait to a struct, but how to tell the compiler that only structs implementing a certain trait are allowed as model (e.g. strategy pattern).

Rust allows you to have TraitObjects (more hands on explanation here). TraitObjects are an abstraction away from the actual struct that is passed or held to the trait/behavior they are implementing. In you example you care that the PricingModel trait is implemented for the Options>>model, so you can tell rust that Options is only allowed to hold structs that -- well -- implement PricingModel.

Since we cannot determine the size of the actual struct at compiletime, we cannot store the struct itself, but a reference to it (hence the Box<dyn Trait> or &dyn Trait).

Thus, your code would look like this:

struct Model;

trait PricingModel {
    fn get_price(&self, opt: &Options) -> f64;
}

impl PricingModel for Model {
    fn get_price(&self, opts:&Options) -> f64 {
        opts.price + 1.0
    }
}

struct Options{
    price: f64,
    model: Box<dyn PricingModel>,//reference to any struct that implements `PricingModel`
}

impl Options {
    fn new(price: f64, model: Box<dyn PricingModel>) -> Self {
        Options {
            price,
            model
        }
    }
}

struct UserModel{multiplier:f64}
impl PricingModel for UserModel{
     fn get_price(&self, opts:&Options) -> f64 {
        (opts.price + 1.0) * self.multiplier
    }
}

fn main() {
    let model = Model;
    let opts = Options::new(1.0, Box::new(model));
    println!("Model: {}", opts.model.get_price(&opts));
    let user_model = UserModel{multiplier: 1.5};
    let user_opts = Options::new(1.0, Box::new(user_model));
    println!("Model: {}", user_opts.model.get_price(&opts));
}

Which compiles :)

On a sidenote (I know that this is not part of the question ;)):

options.model.get_price(&options) seems like a bad code smell: a caller could do options.model.get_price(&other_options) as well... I suggest to have

impl Options {
  //...
  fn get_price(&self)->f64{
    self.model.get_price(/*relevant parameters here*/)
  }
}

so that callers don't have to bother with details: options.get_price()!

Hope this helps and happy coding :)!

Related