I am currently dealing with a question on Rust, hope this is not a too stupid question. The problem I am trying to solve is the following:
I want to have a struct
witch two numeric fields, x
and y
. These numeric fields can be either signed or unsigned. These fields should only be numeric, float or int, but can be signed or unsigned.
The struct
has one method called offset
, which allows to increase or decrease the values of the fields.
However, the behaviour of the offset
method has to change depending on if the fields of the struct
are signed or unsigned, because if the fields are unsigned they can never be negative.
This problem is illustrated below:
use std::ops::AddAssign;
#[derive(Debug)]
struct Coords<T: AddAssign> {
x: T,
y: T,
}
impl<T: AddAssign> Coords<T> {
/// Allows to change the values of the fields
fn offset(&mut self, dx: T, dy: T) {
self.x = dx;
self.y = dy;
}
}
// This works well for signed number. The offset method works for signed integers.
let x: i8 = 0;
let y: i8 = 0;
let mut coords = Coords{ x, y };
coords.offset(-10, -10);
println!("{:?}", coords); // Coords { x: -10, y: -10 }
// However, this does not work if x and y were unsigned
let x: u8 = 0;
let y: u8 = 0;
let mut coords = Coords{ x, y };
// This does not compile.
// Should give the result as Coords { x: 0, y: 0 }
// coords.offset(-10, -10);
I am trying to write an implementation of the offset
method as to handle the scenarios where the fields of Coords
can be either float or int, signed or unsigned. What would be the cleanest and most idiomatic way of solving this?
I tried playing around with traits from the num
crate, but no success so far. I think I could just write two structs, one for unsigned fields and another for signed fields, each of this with a different implementation of the offset
method, but I am pretty sure that there is a more idiomatic solution for this in Rust.
Thank you :)
CodePudding user response:
You could write a trait to represent your number and the allowed operations on it. Something like the following:
trait CoordType : Copy {
type Signed;
fn offset(self, d: Self::Signed) -> Self;
}
You could also add AddAssign
and/or SubAssign
as requirements for this trait, depending on the intended usage, but they are not needed for this simple example, because the offset()
function takes care of everything.
The impls for i8
and u8
would be as follows:
impl CoordType for i8 {
type Signed = i8;
fn offset(self, d: Self::Signed) -> Self {
self.saturating_add(d)
}
}
impl CoordType for u8 {
type Signed = i8;
fn offset(self, d: Self::Signed) -> Self {
if d >= 0 {
self.saturating_add(d as u8)
} else {
self.saturating_sub(-d as u8)
}
}
}
I'm using saturating_add/sub
instead of plain
and -
to avoid panics on overflows. If you need that for i16
, u16
, i32
, u32
, etc. then a macro would help a lot.
The implementations for f32
and f64
should be also quite straightforward.
And then your Coords
type is trivially implemented as:
#[derive(Debug)]
struct Coords<T: CoordType> {
x: T,
y: T,
}
impl<T: CoordType> Coords<T> {
fn offset(&mut self, dx: T::Signed, dy: T::Signed) {
self.x = self.x.offset(dx);
self.y = self.y.offset(dy);
}
}
Here is a link to the playground with that code.