How to create two structs that are identical but of different types

I am learning Rust. As I am a physicist, I am starting with a simple particles simulator, which is something I have previously done in C++. I wrote the following simple code:

#[derive(Debug)]
struct PhysicalVector {
    x: f64,
    y: f64,
    z: f64,
}

impl PhysicalVector {
    fn add(&mut self, other: &PhysicalVector) {
        self.x = self.x + other.x;
        self.y = self.y + other.y;
        self.z = self.z + other.z;
    }
}

#[derive(Debug)]
struct Particle {
    position: PhysicalVector,
    velocity: PhysicalVector,
}

fn main() {
    let mut particle = Particle {
        position: PhysicalVector {x: 0., y: 0., z: 0.},
        velocity: PhysicalVector {x: 0., y: 0., z: 1.},
    };
    
    dbg!(&particle);
    
    particle.position.add(&PhysicalVector{x: 1., y: 0., z: 0.}); // This is ok!
    particle.position.add(&particle.velocity); // This is a mistake, you cannot add velocity and position!!
    
    dbg!(&particle);
}

which works, but is prone to physics mistakes as it uses the same data type for position and for velocity, thus enabling to do things such as particle.position.add(&particle.velocity) which is clearly something that should not compile under the laws of physics, and would it be nice to make it not compile in Rust as well.

The "brute force" solution is to replicate the definition of each different kind of vector, like this:

#[derive(Debug)]
struct Position {
    x: f64,
    y: f64,
    z: f64,
}

impl Position {
    fn add(&mut self, other: &Position) {
        self.x = self.x + other.x;
        self.y = self.y + other.y;
        self.z = self.z + other.z;
    }
}

#[derive(Debug)]
struct Velocity {
    x: f64,
    y: f64,
    z: f64,
}

impl Velocity {
    fn add(&mut self, other: &Velocity) {
        self.x = self.x + other.x;
        self.y = self.y + other.y;
        self.z = self.z + other.z;
    }
}

#[derive(Debug)]
struct Particle {
    position: Position,
    velocity: Velocity,
}

fn main() {
    let mut particle = Particle {
        position: Position {x: 0., y: 0., z: 0.},
        velocity: Velocity {x: 0., y: 0., z: 1.},
    };
    
    dbg!(&particle);
    
    particle.position.add(&Position{x: 1., y: 0., z: 0.}); // This is ok!
    particle.position.add(&particle.velocity); // Now this does not compile, ok!
    
    dbg!(&particle);
}

However, this is obviously not the way to go. What is the correct way of doing this in Rust? I mean, creating two data types Position and Velocity that are identical in all except in the data type?

I was suggested to proceed with generics, which look like C++ templates to me. I wrote this:

use std::marker::PhantomData;

#[derive(Debug)]
struct PhysicalVector<T> {
    x: f64,
    y: f64,
    z: f64,
    _t: PhantomData<T>,
}

impl<T> PhysicalVector<T> {
    fn add(&mut self, other: &Self) {
        self.x = self.x + other.x;
        self.y = self.y + other.y;
        self.z = self.z + other.z;
    }
}

struct Position;
struct Velocity;

fn main() {
    let pos: PhysicalVector<Position> = {x:0., y:0., z:0.};
}

However I cannot figure out how to instantiate one of these PhysicalVector<Position> structures. How can I do this?

You have to initialize the _t too. I suggest making a constructor so you don't have to type it out every time.

    fn new(x: f64, y: f64, z: f64) -> Self {
        Self {
            x,
            y,
            z,
            _t: PhantomData,
        }
    }

Then, let pos = PhysicalVector::<Position>::new(0., 0., 0.);

3 Likes

FYI, the vector math library euclid provides structs that already work exactly like this.

Thanks. How do you instantiate one of such objects? Sorry, I started today with Rust and cannot find examples for dummies.

use euclid;

fn main() {
    enum Position {
        meters,
        kilometers,
    }

    let point = euclid::Vector3D::<f64, Position::meters> {x:0.,y:0.,z:0.};

    dbg!(&point);
}

Of course this fails, but I cannot figure out what is the way of creating a Vector3D.

One possibility would be to wrap the record (the PhysicalVector) in a newtype whose job is to stop you from accidentally mixing it with other categories of the same record:

#[derive(Debug)]
struct PhysicalVector {
    x: f64,
    y: f64,
    z: f64,
}

impl PhysicalVector {
    fn add(&mut self, other: &PhysicalVector ) {
        self.x = self.x + other.x;
        self.y = self.y + other.y;
        self.z = self.z + other.z;
    }
}

#[derive(Debug)]
#[repr(transparent)]
struct Position(PhysicalVector);

impl Position {
  fn add(&mut self, other: &Position) {
    self.0.add(&other.0)
  }
}

#[derive(Debug)]
#[repr(transparent)]
struct Velocity(PhysicalVector);

#[derive(Debug)]
struct Particle {
    position: Position,
    velocity: Velocity,
}

fn main() {
    let mut particle = Particle {
        position: Position(PhysicalVector {x: 0., y: 0., z: 0.}),
        velocity: Velocity(PhysicalVector {x: 0., y: 0., z: 1.}),
    };

    particle.position.add(&Position(PhysicalVector {x: 1., y: 0., z: 0.})); // This is ok!
    // particle.position.add(&particle.velocity); // Now this does not compile, ok!
}

This has the tradeoff that you need to go through and define each individual operation on any combination of Position, Velocity, and/or PhysicalVector that makes sense in your system, rather than having a single generalized type that can perform those, but it does catch the kind of thinko you're concerned about fairly effectively.

1 Like

Enum variantes aren't types. Generics only take types. So what you need to do is the same as in your example, define some types to use.

    struct Meters;
    struct Kilometers;

    let point = euclid::Vector3D::<f64, Meters> {x:0.,y:0.,z:0.};
```
1 Like

You may also find the uom (units of measurements) crate useful. uom - Rust

error[E0063]: missing field `_unit` in initializer of `Vector3D<f64, Meters>`
  --> src/lib.rs:17:21
   |
17 |         let point = euclid::Vector3D::<f64, Meters> { x: 0., y: 0., z: 0. };
   |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `_unit`

For more information about this error, try `rustc --explain E0063`.

There is a phantom field _unit that needs to be specified, just like in their example. Perhaps there is a convenient constructor method that sets this is in Euclid but I don't see it. The Euclid library looks fairly complex. I haven't used it.


Oh, it's just:

let point: euclid::Vector3D<f64, Meters> = euclid::vec3(0., 0., 0.);

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.