Hi,
I’m in the fortunate position of having convinced colleagues that moving a chunk of performance critical code to Rust from Koltin is a plausible way of fixing some of our issues. The first step was prototyping some of the core functionality and benchmarking it which showed significant improvements but I’ve run into a choice that I want to get right.
The Koltin makes very heavy use of interfaces, some of this we’ll discard as it’s over-use but a fair amount we need to retain. This means I’ve used generics in function calls and types (structs) so that we can leverage polymorphism. The issue I have though is that we’ll need to retain the same HTTP API and as such there will be instances where we’ll deserialize from JSON into DTOs from which we’ll need to convert to the internal representations which use generics.
I’ve written a minimal example here:
use serde::{Serialize, Deserialize};
#[derive(Deserialize, Serialize, Debug, Clone,)]
#[serde(rename_all = "camelCase")]
struct UserDto{
pub id: usize,
pub behaviour: BehaviourType
}
impl UserDto{
fn to_user(self) -> User<impl UserBehaviour>{
let behaviour = match self.behaviour{
BehaviourType::FixedBehaviour(fixed) => fixed,
BehaviourType::VariableBehaviour(variable) => variable
};
User { id: self.id, behaviour: behaviour }
}
}
pub trait UserBehaviour{
fn get_behaviour(&self, period: usize) -> f64;
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type",rename_all = "camelCase")]
enum BehaviourType {
FixedBehaviour(FixedBehaviour),
VariableBehaviour(VariableBehaviour),
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct FixedBehaviour{
fixed_amount: f64
}
impl UserBehaviour for FixedBehaviour{
fn get_behaviour(&self, _period: usize) -> f64 {
self.fixed_amount
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct VariableBehaviour{
variable_amount: f64
}
impl UserBehaviour for VariableBehaviour{
fn get_behaviour(&self, period: usize) -> f64 {
self.variable_amount * period as f64 / 3.0
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct User<B: UserBehaviour>{
pub id: usize,
pub behaviour: B
}
fn forecast_user<B: UserBehaviour>(user: User<B>){
let user_id = user.id;
println!("Forecasting for user with id: {user_id}");
let user_behaviour = user.behaviour.get_behaviour(0);
println!("User behaviour is : {user_behaviour}");
}
fn main() {
let user_1_json = r#"{"id":1,"behaviour":{"type":"fixedBehaviour","fixedAmount":12.0}}"#;
let user_1_dto: UserDto = serde_json::from_str(user_1_json).unwrap();
let user_1 = user_1_dto.to_user();forecast_user(user_1);
forecast_user(user_1);
}
In short my issue is that in the struct ‘UserDto’ which will be the body of say a PUT request I need the key ‘behaviour’ to be a union of a few concrete types that al implement the trait ‘UserBehaviour’ this is easy enough to sort using serde. The issue arises when I want to convert this ‘UserDto’ into the internal representation ‘User’ which is actually used by the core functionality of ‘forecast_user’. Doing the conversion using a generic isn’t possible using a match statement as in the example because the two arms return different types. The most obvious solution to me is to use Box dyn UserBehaviour but this comes with (a fairly small) performance overhead and I’m concerned that it would effectively be a crutch and I’d be using a lost of Box dyn traits anywhere I needed polymorphism rather than generics.. Is there a better / more idiomatic way of solving this?
Thanks!