Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generics partial specialization in Rust

Tags:

generics

rust

Let's look at some example of mathematics vector. It consists of a different number of components depending on the space dimension.

  • For 2D: x, y;
  • For 3D: x, y, z;
  • For 4D: x, y, z, w;
  • Generic: N components.

In C++ I can use SFINAE concept to implement it.

template <size_t D, typename T, typename = void>
struct Vector;

// Implement for 2D
template<size_t D, typename T>
struct Vector <D, T, std::enable_if_t<(D == 2)>>
{
    T x;
    T y;
}

// Implement for 3D
template<size_t D, typename T>
struct Vector <D, T, std::enable_if_t<(D == 3)>>
{
    T x;
    T y;
    T z;
}
    
// Implement for 4D
template<size_t D, typename T>
struct Vector <D, T, std::enable_if_t<(D == 4)>>
{
    T x;
    T y;
    T z;
    T w;
}

How can I do the same in Rust?

like image 306
Edward Sarkisyan Avatar asked Mar 27 '21 15:03

Edward Sarkisyan


2 Answers

You cannot specialize generics in Rust like specializing templates in C++. (Rust has a feature called "specialization", but it only applies to impls, and it's not really relevant here.) Rust generics are sometimes called "principled" because they have to work in principle (upon declaration), not just in practice (once instantiated). This is a deliberate choice on the part of Rust's designers to avoid some of the messier consequences of SFINAE in C++.

I can think of two main ways to achieve a similar effect to your C++ code in Rust, depending on the generic context of the code. One way is to use a trait as a type level function to compute the content type of a parameterized struct, which is similar to the C++ version but has slightly more verbose field access (for simplicity, I'll imagine that T is f32 for these examples):

// types that contain the actual data
struct Vector2 {
    x: f32,
    y: f32,
}

struct Vector3 {
    x: f32,
    y: f32,
    z: f32,
}

// types that will be used to parameterize a type constructor
struct Fixed<const N: usize>;
struct Dynamic;

// a type level function that says what kind of data corresponds to what type
trait VectorSize {
    type Data;
}

impl VectorSize for Fixed<2> {
    type Data = Vector2;
}

impl VectorSize for Fixed<3> {
    type Data = Vector3;
}

impl VectorSize for Dynamic {
    type Data = Vec<f32>;
}

// pulling it all together
struct Vector<Z>(Z::Data) where Z: VectorSize;

Now, if you have v: Vector<Fixed<2>> you can use v.0.x or v.0.y, whereas if you have a Vector<Dynamic> you have to use v.0[0] and v.0[1]. But there's no way to write a generic function that uses x and y that will work with either Vector<Fixed<2>> or Vector<Fixed<3>>; since there's no semantic relationship between those xs and ys, that would be unprincipled.

Another option would be putting an array in Vector and making x and y convenience methods that access elements 0 and 1:

struct Vector<const N: usize> {
    xs: [f32; N],
}

impl<const N: usize> Vector<N> {
    fn x(&self) -> f32 where Self: SizeAtLeast<2> {
        self.xs[0]
    }

    fn y(&self) -> f32 where Self: SizeAtLeast<2> {
        self.xs[1]
    }
    
    fn z(&self) -> f32 where Self: SizeAtLeast<3> {
        self.xs[2]
    }
}

// In current Rust, you can't bound on properties of const generics, so you have
// to do something like this where you implement the trait for every relevant
// number. Macros can make this less tedious. In the future you should be able to
// simply add bounds on `N` to `x`, `y` and `z`.
trait SizeAtLeast<const N: usize> {}

impl SizeAtLeast<2> for Vector<2> {}
impl SizeAtLeast<2> for Vector<3> {}
impl SizeAtLeast<2> for Vector<4> {}

impl SizeAtLeast<3> for Vector<3> {}
impl SizeAtLeast<3> for Vector<4> {}

Now you can write generic functions that work for Vector<N> and use x and y, but it's not as easy to adapt this to allow mutation. One way to do so is to add x_mut, y_mut and z_mut methods that return &mut f32.

Related question

  • Equivalent of specific template usage in C++ for Rust
like image 112
trent Avatar answered Oct 25 '22 16:10

trent


Failing that, tuple types may become handy here. You could define the generic Vector struct below, which is aimed at taking a tuple type as the generic type argument:

struct Vector<T> {
   pub coord: T,
}

impl<T> Vector<T> {
   fn new(coord: T) -> Self {
      Self { coord }
   }
}

Then, as an example, if (f32, f32) is the type argument for Vector, you will obtain a two-dimensional vector:

let v2d /* : Vector<(f32,f32)> */ = Vector::new((1.0, 2.0)); value

println!("v2 = ({}, {})", v2d.coord.0, v2d.coord.1);

Similarly for three dimensions (i.e., T = (f32, f32, f32)):

type V3d = Vector<(f32, f32, f32)>;

let v3d = V3d::new((1.0, 2.0, 3.0));

...and so on.

like image 20
ネロク・ゴ Avatar answered Oct 25 '22 15:10

ネロク・ゴ