Struct common functionality

Hello, I am trying add common functionality to my Composition types as they all have ing_id, fraction/percentage, action_id, action_count. Common functionality will prevents unnecessary repetition, as I don't want to write a separate function for each of them. My functions handle checks, comparisons, and some modifications.

Here are the types:

#[derive(Debug, Clone, Copy,)]
pub struct CompositionWithoutMixId {
	pub fraction: f64,
	pub ingredient_id: i64,
	pub action_id: i64,
	pub action_counter: i32,
}

pub type Composition = Vec<CompositionWithoutMixId>;

#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
pub struct CompositionFormPart {
	pub ingredient_id: i64,
	pub percentage: f64, // 100 is max
	pub action_id: Option<i64>,
	pub action_counter: Option<i32>,
}

pub type CompositionForm = Vec<CompositionFormPart>;

#[derive(Serialize, Default, Debug)]
pub struct CompositionInterface {
	pub fraction: f64,
	pub mix_id: i64,
	pub ingredient_id: i64,
	pub action_id: i64,
	pub action_counter: i32,

	pub action_name: String,

	pub ingredient_name: String,
	pub ingredient_version_group: i64,
	pub ingredient_version: i64,
	pub ingredient_deleted: bool,
	pub ingredient_comment: Option<String>,
}

pub type MixCompositionInterface = Vec<CompositionInterface>;

Currently I have found a solution where:

I implement a getter trait:

trait CompositionTrait {
	fn fraction(&self) -> f64;
	fn ingredient_id(&self) -> i64;
	fn action_id(&self) -> i64;
	fn action_counter(&self) -> i32;
}

Various Into implementations for mut access:

// This struct is used in some implementations
pub struct MixComposition {
	pub mix_id: i64,
	pub composition: Composition,
}
impl Into<Vec<MixCompositionForCreate>> for MixComposition {
	fn into(self) -> Vec<MixCompositionForCreate> {
		self.composition
			.into_iter()
			.map(|cp| MixCompositionForCreate {
				mix_id: self.mix_id,
				fraction: cp.fraction,
				ingredient_id: cp.ingredient_id,
				action_id: cp.action_id,
				action_counter: cp.action_counter,
			})
			.collect()
	}
}

pub type CompositionForm = Vec<CompositionFormPart>;
impl From<CompositionFormPart> for CompositionWithoutMixId {
	fn from(value: CompositionFormPart) -> CompositionWithoutMixId {
		CompositionWithoutMixId {
			fraction: value.percentage / 100.0,
			ingredient_id: value.ingredient_id,
			action_id: value.action_id.unwrap_or(ActionTypeEnum::default().into()),
			action_counter: value.action_counter.unwrap_or(1),
		}
	}
}

pub fn form_into_comp(form_comp: CompositionForm) -> Composition {
	form_comp
		.into_iter()
		.map(|p| CompositionWithoutMixId {
			fraction: p.percentage / 100.0,
			ingredient_id: p.ingredient_id,
			action_id: p.action_id.unwrap_or(ActionTypeEnum::default().into()),
			action_counter: p.action_counter.unwrap_or(1),
		})
		.collect()
}
pub fn comp_into_form(comp: Composition) -> CompositionForm {
	comp.into_iter()
		.map(|p| CompositionFormPart {
			percentage: p.fraction * 100.0,
			ingredient_id: p.ingredient_id,
			action_id: Some(p.action_id),
			action_counter: Some(p.action_counter),
		})
		.collect()
}


impl From<CompositionInterface> for CompositionWithoutMixId {
	fn from(value: CompositionInterface) -> CompositionWithoutMixId {
		CompositionWithoutMixId {
			fraction: value.fraction,
			ingredient_id: value.ingredient_id,
			action_id: value.action_id,
			action_counter: value.action_counter,
		}
	}
}

Finnaly I use impl Into or impl CompositionTrait as my function's parameters:

part: & impl CompositionTrait
// or 
part: impl Into<CompositionWithoutMixId>

Is there a better way to do this?
I think I am achieving common functionality in a pretty obscure way...

Hmm, after reading your question, I’m not quite sure what specific problem you’re trying to solve.

In general, when creating types, abstractions, or concepts, a good guideline is that each wrapper or layer of indirection should address a concrete problem and provide more benefits than it introduces complexity or overhead.

Of course, what counts as a benefit (or a cost) can be subjective. Sometimes, thinking in terms of the benefit-to-cost ratio might help clarify whether an abstraction is worthwhile.

1 Like

I'm implementing common functionality to allow different structs to be used in functions that only need core attributes like ingredient_id, fraction/percentage, action_id, and action_count. This prevents unnecessary repetition, as I don't want to write a separate function for each of them. My functions handle checks, comparisons, and some modifications.

Hmm, sure—I can only guess::

In that case, using a trait is a common and effective solution:

struct A;
struct B;

trait SomeTrait { ... }

impl SomeTrait for A { ... }
impl SomeTrait for B { ... }

fn example(x: &impl SomeTrait) { ... }

I assume this part is clear.

Instead of extracting data from unknown objects and then performing calculations separately:

struct SomeThing;

struct SomeData { ... }

trait ExtractData {
    fn get_data(&self) -> SomeData {
        SomeData {
            ...
        }
    }
}

impl ExtractData for SomeThing { ... }

fn example(x: &impl ExtractData) {
    let data = x.get_data();
    todo!("use the data");
}

You could move the functionality directly into the trait itself, simplifying the process:

struct SomeThing;

trait Example {
    fn example(&self);
}

impl Example for SomeThing {
    fn example(&self) {
        todo!("work with SomeThing directly");
    }
}

Regarding this:

For comparisons, you can implement comparison traits like PartialEq and PartialOrd directly for your types—even across different types. Both traits support a RHS (right-hand side) type parameter to allow comparisons between distinct types. You might want to check out the documentation for more details: std::cmp - Rust

2 Likes

I guess I can move all getters into one big one.

trait CompositionTrait {
    fn as_composition(&self) -> CompositionWithoutMixId;
}

impl CompositionTrait for CompositionInterface {
    fn as_composition(&self) -> CompositionWithoutMixId {
        CompositionWithoutMixId {
            fraction: self.fraction,
            ingredient_id: self.ingredient_id,
            action_id: self.action_id,
            action_counter: self.action_counter,
        }
    }
}

But will this produce any over head?

Sure.

That said, it’s generally a good idea for a trait to group related functionality together. If your getters are closely related and logically belong together, combining them in a single trait makes perfect sense.

However, if the operations can be clearly divided into distinct groups, using separate traits might be more beneficial. This way, you can implement only the relevant traits for specific types, keeping your code more modular and flexible.

What kind of overhead are you concerned about?

Modern compilers—especially Rust's—perform extensive optimizations. In most cases, it’s better to focus on writing clear, maintainable code and only start optimizing if you encounter actual performance issues in real-world usage. Even then, the first step should be to measure where the bottlenecks are and focus on optimizing those specific areas.

Practically speaking, using traits in Rust doesn’t introduce any noticeable overhead.

1 Like