Especialización parcial en genéricos en Rust

CorePress2024-01-25  8

Veamos algún ejemplo de vector matemático. Se compone de un número diferente de componentes dependiendo de la dimensión del espacio.

Para 2D: x, y; Para 3D: x, y, z; Para 4D: x, y, z, w; Genérico: N componentes.

En C++ puedo usar el concepto SFINAE para implementarlo.

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;
}

¿Cómo puedo hacer lo mismo en Rust?

12

Realmente no tenemos SFINAE en Rust. Las plantillas de C++ son en realidad un mecanismo de generación de código disfrazado de genéricos, mientras que los genéricos en... bueno, básicamente cualquier otro lenguaje, son un tipo de sistema.Mecanismo de raíz para polimorfismo paramétrico. Alguien con más experiencia en Rust puede venir y encontrar una manera de hackearlo, pero ciertamente el enfoque idiomático en Rust sería usar diferentes estructuras (Vector2, Vector3, Vector4) y luego colocar cualquier funcionalidad común en un rasgo (rasgo Vector {...})

-Silvio Mayolo

27/03/2021 a las 15:23



------------------------------------

No puedes especializar genéricos en Rust como especializar plantillas en C++. (Rust tiene una característica llamada "especialización", pero solo se aplica a impls y no es realmente relevante aquí). Los genéricos de Rust a veces se denominan "principios". porquePor lo tanto, tienen que funcionar en principio (tras la declaración), no sólo en la práctica (una vez instanciadas). Esta es una elección deliberada por parte de los diseñadores de Rust para evitar algunas de las consecuencias más complicadas de SFINAE en C++.

Se me ocurren dos formas principales de lograr un efecto similar a su código C++ en Rust, dependiendo del contexto genérico del código. Una forma es usar un rasgo como función de nivel de tipo para calcular el tipo de contenido de una estructura parametrizada, que es similar a la versión C++ pero tiene un acceso a campos un poco más detallado (para simplificar, imaginaré que T es f32 para estos ejemplos):

// 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;

Ahora, si tienes v: Vector<Fixed<2>> puedes usar v.0.x o v.0.y, mientras que si tienes un Vector<Dynamic> tienes que usar v.0[0] y v.0[1]. Pero no hay manera de escribiruna función genérica que usa xey y que funcionará con Vector<Fixed<2>> o Vector<Fijo<3>>; dado que no existe una relación semántica entre esas x e y, eso sería una falta de principios.

Otra opción sería poner una matriz en Vector y crear métodos convenientes x e y que accedan a los elementos 0 y 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> {}

Ahora puedes escribir funciones genéricas que funcionen para Vector<N> y use xey, pero no es tan fácil adaptar esto para permitir la mutación. Una forma de hacerlo es agregar los métodos x_mut, y_mut y z_mut que devuelvan &mut f32.

Pregunta relacionada Equivalente al uso de plantillas específicas en C++ para Rust

2

Estoy muy decepcionado. Pensé que Rust soporta SFINAE como C++. Muchas gracias por respuestas tan detalladas, me ayudaste mucho.

-Edward Sarkisyan

28/03/2021 a las 13:46

gracias por la información. Con eso en mente, pude resolver adecuadamente mi problema de diseño de estructuras.

-zertyz

2 de septiembre de 2021 a las 21:53



------------------------------------

De lo contrario, los tipos de tuplas pueden resultar útiles aquí. Podrías definir la estructura vectorial genérica a continuación, cuyo objetivo es tomar un tipo de tupla como argumento de tipo genérico:

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

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

Entonces, como ejemplo, si (f32, f32) es el argumento de tipo para Vector, obtendrás un vector bidimensional:

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

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

De manera similar para tres dimensiones (es decir, T = (f32, f32, f32)):

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

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

...y así sucesivamente.

Su guía para un futuro mejor - libreflare
Su guía para un futuro mejor - libreflare