How to implement strategies selected at run time in Rust?

Viewed 102

I would like to write a simulation algorithm in Rust that has three main parts. The first part is a struct that maintains the current state of the system and associated methods to make allowed state transitions. The second part is a simulation strategy, this tells which state transition(s) to make next (e.g. I will have a slow but accurate simulation strategy and a quick but approximate strategy). Finally, I have a way to serialise the system after a simulation step was taken (e.g. one method writes the system to csv while another one to json).

I would like to choose both the simulation strategy and the serialisation method at run time.

Coming from Python, my first try for the main simulation loop looked like this:

let mut system = System { ... };
let simulator = GillespieAlgorithm { system: &mut system, ... };  // this will be dynamic eventually
let output_formatter = CSVFormatter { system: &system, ... };  // this will be dynamic eventually

output_formatter.start();

for _ in 1..100 {
  simulator.step();
  output_formatter.write_current_state();
}

output_formatter.stop();

Of course, this doesn't work because I want to borrow system twice with one of them being a mutable reference.

What would be the idiomatic Rust solution to this problem? My understanding is that I should somehow attach a behaviour dynamically to System instead of passing system to other structs.

1 Answers

You've mentioned in the comments, that you want to keep the &mut system in your simulation. Thats fine and you can still use system, as long as you're getting it via the GillespieAlgorithm. If you're fine to pass it to the formatter by method argument rather than constructor, this might be a solution for you (playground)

struct System();
struct GillespieAlgorithm<'a> { system: &'a mut System }
struct CSVFormatter();

fn main() {
    let mut system = System();
    let mut simulator : Box<dyn Algorithm> = Box::new(GillespieAlgorithm { system: &mut system });  // this will be dynamic eventually
    let output_formatter: Box<dyn Formatter> = Box::new(CSVFormatter());  
    output_formatter.start();

    for _ in 1..100 {
      simulator.step();
      output_formatter.write_current_state(simulator.borrow_system());
    }
    
    output_formatter.stop();
}

trait Algorithm {
    fn step(&mut self) {}
    fn borrow_system(&self) -> &System;
}

impl<'a> Algorithm for GillespieAlgorithm<'a> {
    fn step(&mut self) {}
    fn borrow_system(&self) -> &System {
        self.system
    }
}

trait Formatter {
    fn start(&self);
    fn write_current_state(&self, system: &System);
    fn stop(&self);
}

impl Formatter for CSVFormatter {
    fn start(&self) {}
    fn write_current_state(&self, _system: &System) {}
    fn stop(&self) {}
}

If you don't need the entire System in the CSVFormatter, you could also return a dedicated state struct in step() and pass this to your formatter.

Related