I'm learning Rust and a few questions did arise during translation of my C++ code to Rust. There are comments in Rust code I'd like to be answered. Is there an idiomatic way to solve this task? The task was in simulating a random process - there are two chairs, which have different processing capacity and there is a flow of customers, who visit the chairs sequentially.
Summary: Shoe shine shop has two chairs, one for brushing (1) and another for polishing (2). Customers arrive according to PP with rate \$\lambda\$, and enter only if first chair is empty. Shoe-shiners takes \$\exp(\mu_1)\$ time for brushing and \$\exp(\mu_2)\$ time for polishing.
Code in C++:
#include <map>
#include <string>
#include <random>
#include <iostream>
#include <numeric>
#include <algorithm>
#include <queue>
int main(int argc, char *argv[]) {
if (argc < 5) {
std::cerr << "not enough arguments!\nlambda, m1, m2, max_time";
return -1;
}
using distribution_t = std::exponential_distribution<double>;
std::string event_names[3] = {"ARRIVED", "FIRST_FINISHED", "SECOND_FINISHED"};
std::string state_names[7] = {"EMPTY", "FIRST", "SECOND", "WAITING",
"BOTH", "DROP", "INVALID"};
enum event_t { ARRIVED = 0, FIRST_FINISHED, SECOND_FINISHED };
enum state_t { EMPTY = 0, FIRST, SECOND, WAITING, BOTH, DROP, INVALID };
std::size_t state_to_clients[DROP] = {0, 1, 1, 2, 2};
// clang-format off
// EMPTY FIRST SECOND WAITING BOTH
state_t event_to_state[3][5] = {
/* ARRIVED */ {FIRST, DROP, BOTH, DROP, DROP},
/* FIRST_FINISHED */ {INVALID, SECOND, INVALID, INVALID, WAITING},
/* SECOND_FINISHED */ {INVALID, INVALID, EMPTY, SECOND, FIRST},
};
// clang-format on
double lambda = atof(argv[1]);
double m1 = atof(argv[2]);
double m2 = atof(argv[3]);
double time_max = atof(argv[4]);
std::mt19937_64 generator(std::random_device{}());
struct stats_t {
std::size_t state_counts[DROP]{}; // max feasible event - BOTH
std::size_t state_counts_with_drop[DROP]{};
double time_in_state[DROP]{};
double time_in_client[3]{}; // roflanEbalo
double served_time = 0.0;
std::size_t served_clients = 0;
std::size_t arrived_clients = 0;
std::size_t dropped_clients = 0;
} stats;
double times[3]{};
distribution_t dists[3] = {distribution_t(lambda), distribution_t(m1),
distribution_t(m2)}; // mean = 1/param
std::map<double, event_t> timeline;
auto inserter = [&timeline, &generator](event_t event, double &t,
distribution_t &dist) {
double dt;
do {
dt = dist(generator);
} while (!timeline.try_emplace(t + dt, event).second);
t += dt;
};
for (std::size_t i = 0; i < 3; ++i)
while (times[event_t(i)] < time_max)
inserter(event_t(i), times[i], dists[i]);
double prev = 0;
state_t state = EMPTY;
std::queue<double> arriving_times;
for (auto [time, event] : timeline) {
if (argc > 5) {
std::cout << "[PROCESSING]: " << time << " " << event_names[event]
<< std::endl;
std::cout << "[INFO]: " << state_names[state] << std::endl;
}
if (event == ARRIVED)
++stats.arrived_clients;
state_t new_state = event_to_state[event][state];
switch (new_state) {
case INVALID:
break;
case DROP:
++stats.state_counts_with_drop[state];
++stats.dropped_clients;
break;
default:
if (event == ARRIVED)
arriving_times.push(time);
else if (event == SECOND_FINISHED) {
stats.served_time += time - arriving_times.front();
arriving_times.pop();
++stats.served_clients;
}
stats.time_in_state[state] += time - prev;
stats.time_in_client[state_to_clients[state]] += time - prev;
prev = time;
state = new_state;
++stats.state_counts[state];
break;
}
}
std::transform(std::begin(stats.state_counts), std::end(stats.state_counts),
std::begin(stats.state_counts_with_drop),
std::begin(stats.state_counts_with_drop),
std::plus<std::size_t>());
auto report = [&state_names](std::string_view title, auto counts) {
std::cout << title << std::endl;
auto events = std::accumulate(counts, counts + DROP, 0.0);
for (std::size_t i = 0; i < DROP; ++i)
std::cout << state_names[i] << ": " << counts[i] / double(events)
<< std::endl;
std::cout << std::endl;
};
report("time in states: ", stats.time_in_state);
report("entries in states: ", stats.state_counts);
report("entries in states with dropouts: ", stats.state_counts_with_drop);
std::cout << "dropout: "
<< stats.dropped_clients / double(stats.arrived_clients)
<< std::endl;
std::cout << "average serving time: "
<< stats.served_time / double(stats.served_clients) << std::endl;
std::cout << "average number of clients: "
<< (stats.time_in_client[1] + 2 * stats.time_in_client[2]) /
std::accumulate(std::begin(stats.time_in_client),
std::end(stats.time_in_client), 0.0)
<< std::endl;
// arr=(10 10 10); for i in {0..2}; do for param in {1..100}; do
// darr=("${arr[@]}"); darr[i]=${param}; echo "${darr[@]}" >> ../out.txt &&
// ./lab2.exe ${darr[@]} 1000000 >> ../out.txt; done; done
}
Code in Rust:
use std::collections::BTreeMap;
use std::collections::VecDeque;
use std::env;
extern crate rand;
use rand::distributions::*;
extern crate ordered_float;
pub use ordered_float::*;
// variant is never constructed: `FirstFinished`, why do I get this message? I can see this variant printed when running the program
#[derive(Copy, Clone, Debug, PartialEq)]
enum Events {
Arrived = 0,
FirstFinished,
SecondFinished,
}
#[derive(Copy, Clone, Debug, PartialEq)]
enum States {
Empty = 0,
First,
Second,
Waiting,
Both,
Dropping,
Invalid,
}
#[rustfmt::skip]
#[derive(Debug, Default)]
struct Stats {
state_counts: [u32; States::Dropping as usize],
state_counts_with_drop: [u32; States::Dropping as usize],
time_in_state: [f64; States::Dropping as usize],
time_in_client: [f64; 3],
served_time: f64,
served_clients: u32,
arrived_clients: u32,
dropped_clients: u32,
}
// 1 template function for this? Or any other way to cast integer to enum? Or I should use libraries for this?
impl From<usize> for States {
fn from(s: usize) -> States {
let tmp: u8 = s as u8;
unsafe { std::mem::transmute(tmp) }
}
}
impl From<usize> for Events {
fn from(s: usize) -> Events {
let tmp: u8 = s as u8;
unsafe { std::mem::transmute(tmp) }
}
}
//what do I need lifetime 'a for? Is there supertrait that specifies multiple traits? ("Number", "container", idk)
//Or can I just say that allowed types are f64 and i32?
fn report<'a, T>(title: &str, counts: &'a [T; States::Dropping as usize])
where
T: std::iter::Sum<&'a T> + std::ops::Div + Copy + Into<f64> + std::fmt::Display,
{
println!("{}", title);
let events: T = counts.iter().sum();
for i in 0..(States::Dropping as usize) {
println!(
"{:?}: {}",
Into::<States>::into(i),
Into::<f64>::into(counts[i]) / Into::<f64>::into(events) // How to call Into properly? this looks bad
);
}
println!();
}
fn main() {
let state_to_clients: [usize; States::Dropping as usize] = [0, 1, 1, 2, 2];
#[rustfmt::skip]
let event_to_state: [[States; 5]; 3] = [
// EMPTY FIRST SECOND WAITING BOTH
/* Arrived */ [States::First, States::Dropping, States::Both, States::Dropping, States::Dropping],
/* First_Finished */ [States::Invalid, States::Second, States::Invalid, States::Invalid, States::Waiting],
/* Second_Finished */ [States::Invalid, States::Invalid, States::Empty, States::Second, States::First],
];
let args: Vec<String> = env::args().collect();
if args.len() < 5 {
panic!("Not enough arguments!");
}
let (lambda, m1, m2, time_max) = (
args[1].parse::<f64>().unwrap(),
args[2].parse::<f64>().unwrap(),
args[3].parse::<f64>().unwrap(),
args[4].parse::<f64>().unwrap(),
);
let mut rng = rand::thread_rng();
let mut stats = Stats::default();
let mut times: [f64; 3] = Default::default();
let mut dists: [Exp; 3] = [Exp::new(lambda), Exp::new(m1), Exp::new(m2)];
// I don't like OrderedFloat because it's a wrapper. Is there a way to implement Ord for floats and keep nice syntax?
// Maybe it's the problem of algorithm. Any proposals?
let mut timeline: BTreeMap<OrderedFloat<f64>, Events> = BTreeMap::new();
let mut inserter = |event: &Events, t: &mut f64, distribution: &mut Exp| {
let mut dt;
//Is it ok to emulate do while loops like this?
while {
dt = OrderedFloat(distribution.sample(&mut rng));
let key = OrderedFloat(*t + Into::<f64>::into(dt));
match timeline.get(&key) {
Some(_) => true,
None => {
timeline.insert(key, *event);
false
}
}
} {}
*t += Into::<f64>::into(dt);
};
for i in 0..3 {
while times[i] < time_max {
inserter(&i.into(), &mut times[i], &mut dists[i]);
}
}
let mut prev = 0f64;
let mut state = States::Empty;
let mut arriving_times = VecDeque::<f64>::new();
for (time, event) in timeline {
if args.len() > 5 {
println!("[PROCESSING]: {} {:?}", time, event);
println!("[INFO]: {:?}", state);
}
if event == Events::Arrived {
stats.arrived_clients += 1;
}
let new_state = event_to_state[event as usize][state as usize];
match new_state {
States::Dropping => {
stats.state_counts_with_drop[state as usize] += 1;
stats.dropped_clients += 1;
}
States::Invalid => (),
_ => {
if event == Events::Arrived {
arriving_times.push_back(Into::<f64>::into(time));
} else if event == Events::SecondFinished {
stats.served_time += Into::<f64>::into(time) - arriving_times.front().unwrap();
arriving_times.pop_front();
stats.served_clients += 1;
}
stats.time_in_state[state as usize] += Into::<f64>::into(time) - prev;
stats.time_in_client[state_to_clients[state as usize] as usize] +=
Into::<f64>::into(time) - prev;
prev = Into::<f64>::into(time);
state = new_state;
stats.state_counts[state as usize] += 1;
}
};
}
for (i, element) in stats.state_counts_with_drop.iter_mut().enumerate() {
*element += stats.state_counts[i];
}
report("time in states: ", &stats.time_in_state);
report("entries in states: ", &stats.state_counts);
report(
"entries in states with dropouts: ",
&stats.state_counts_with_drop,
);
println!(
"dropout: {}\naverage serving time: {}\naverage number of clients: {}",
(stats.dropped_clients as f64) / (stats.arrived_clients as f64),
stats.served_time / (stats.served_clients as f64),
(stats.time_in_client[1] + 2.0f64 * stats.time_in_client[2])
/ stats.time_in_client.iter().sum::<f64>()
);
}