Search code examples
genericsrustsimulationtraitsbuilder

How do I implement this API? It's a Builder pattern that takes generic (or trait) params


Scenario

Imagine you're a baker. You bake different cakes using different recipes.

Goal

You're trying to plan how many ingredients you'll need over the next 6m using your upcoming list of orders.

The challenge is, your orders aren't certain. They're based on a probability.

Problem

I've started hallo – a library to run these kind of simulations. I'm stuck on how to implement the ergonomics of the API I've imagined. Here's what I have in mind:

struct Sugar(f32);
struct Chocolate(f32);
struct Vanilla(f32);

let brownieRecipe = RecipeBuilder::default()
    .add(Sugar(200.0))
    .add(Chocolate(10.0))
    .build();

let vanillaSponge = RecipeBuilder::default()
    .add(Sugar(300.0))
    .add(Vanilla(2.0))
    .build();

let p1 = AllocationBuilder::default()
    .recipe(brownieRecipe)
    .duration_weeks(5)
    .start_date(&(today + Duration::weeks(8)))
    .build();

let p2 = AllocationBuilder::default()
    .recipe(vanillaSponge)
    .duration_weeks(2)
    .start_date(&(today + Duration::weeks(2)))
    .build();

The RecipeBuilder builds a recipe from any number of ingredients. The AllocationBuilder takes a recipe and plans when it's needed.

The next step is to implement a simulation engine to plot 10k outcomes (but that's out of scope of this question).

The ingredient structs might eventually implement some trait so that they can be summed (std::Add?).


Solution

  • You'll need to have some trait for using the ingredients. What trait - that depends on what you need to do with them. It can be a builtin trait or a custom trait, but it is important that all ingredients (Sugar, Chocolate etc.) will implement it (I'd go with a custom trait).

    Assuming you have a trait Ingredient, store a list of Vec<Box<dyn Ingredient>>. For the API to be what you described, you need the add() method to be defined like:

    pub fn add<I: Ingredient>(&mut self, ingredient: I) -> &mut Self {
        self.ingredients.push(Box::new(ingredient));
        self
    }
    

    This is a convenience method: instead of needing to pass Box<dyn Ingredient>, you pass impl Ingredient and the method creates the Box<dyn Ingredient>. This is a very common pattern in Rust.