Historia: Los genéricos constantes se estabilizaron en Rust 1.51 para permitir que los tipos se parametrizaran por valores constantes de tipos enteros primitivos, lo que permite arreglos de tamaño fijo genéricos como [T; N]. Durante la fase de diseño, el equipo del lenguaje restringió explícitamente los parámetros genéricos constantes a tipos que exhiben igualdad estructural y evaluación determinista en tiempo de compilación. Esta restricción excluyó f32, f64 y literales de &str debido a su violación del orden total o su dependencia de direcciones de memoria en tiempo de ejecución.
Problema: El problema fundamental con los tipos de punto flotante es la presencia de NaN (Not-a-Number), que viola la igualdad reflexiva (NaN != NaN), impidiendo que el compilador determine de manera confiable la identidad del tipo durante la monomorfización. Para los literales de cadena (&str), el problema radica en su representación de puntero fat (dirección + longitud) y su dependencia de direcciones de memoria específicas en el segmento de datos, que no son deterministas entre unidades de compilación o crates. El sistema de tipos requiere que MyStruct<1> y MyStruct<1> se refieran siempre al mismo tipo, lo que exige que la igualdad del parámetro constante sea decidible a través de comparación por bits o estructural en tiempo de compilación.
Solución: El compilador de Rust hace cumplir estas restricciones a través de rasgos internos como StructuralPartialEq (inestable) durante la reducción a HIR (Representación Intermedia de Alto Nivel) y la verificación de tipos. Al encontrar un parámetro genérico constante, el compilador verifica que el tipo sea un entero, bool o char, o un tipo definido por el usuario marcado explícitamente como compatible con la igualdad estructural. Rechaza los tipos de punto flotante porque su igualdad no es reflexiva y rechaza las referencias como &str porque introducen complejidades de vida útil e indirección que no se pueden reconciliar en el contexto de 'static requerido para genéricos constantes. Durante la monomorfización, el compilador evalúa expresiones constantes y utiliza la igualdad estructural para fusionar instanciaciones idénticas, asegurando la seguridad de tipos.
// Válido: usize tiene igualdad estructural struct Matrix<const N: usize> { data: [[f64; N]; N], } // Inválido: f64 carece de un orden total (problemas de NaN) // struct Physics<const G: f64>; // Error: los tipos de punto flotante no pueden usarse en genéricos constantes // Inválido: &str tiene complejidades de indirecto y vida útil // struct Label<const S: &str>; // Error: `&str` está prohibido como tipo de un parámetro genérico constante
Estás diseñando un motor de negociación de alta frecuencia donde los instrumentos financieros deben llevar parámetros constantes en tiempo de compilación para especificaciones de contratos, como los tamaños de ticks (por ejemplo, 0.25 USD) o coeficientes multiplicadores. El diseño inicial intentó usar genéricos constantes f64 para codificar estos valores decimales precisos directamente en el sistema de tipos, esperando eliminar el almacenamiento en tiempo de ejecución de estas constantes y permitir la optimización en tiempo de compilación de los cálculos de precios.
Una de las aproximaciones consideradas fue eludir la restricción mediante la transmutación de bits de f64 a u64 y usar eso como el parámetro constante, luego volviendo a transmutar dentro de la implementación. Sin embargo, esto resultó peligroso porque los flotantes idénticos a nivel de bits pueden representar diferentes valores semánticos debido al cero firmado (+0.0 vs -0.0) y a los payloads de NaN, lo que podría hacer que el compilador tratase distintos instrumentos financieros como el mismo tipo o fusionara cálculos que deberían permanecer separados, llevando a una lógica de precios incorrecta.
Otra solución involucró usar constantes asociadas dentro de un rasgo (trait Instrument { const TICK_SIZE: f64; }). Si bien esto permite valores de punto flotante, sacrifica la capacidad de usar el tamaño de tick como un discriminador a nivel de tipo; no puedes tener Vec<Instrument<TICK_SIZE>> conteniendo diferentes instrumentos con diferentes tamaños de tick sin recurrir a los objetos dyn Trait, los cuales introducen indireccionamiento de vtable inaceptable en la ruta caliente.
La solución elegida fue codificar los valores de punto flotante como números enteros de punto fijo (por ejemplo, representando 0.25 USD como el usize 25 con un factor de escalado implícito de 100). Este enfoque satisface las restricciones de los genéricos constantes mientras mantiene una abstracción de costo cero y evaluación en tiempo de compilación. El resultado fue un sistema de contratos seguro por tipos donde Bond<25> y Bond<50> son tipos distintos sin sobrecarga en tiempo de ejecución, aunque requirió una cuidadosa documentación de la convención de escalado para prevenir errores aritméticos.
¿Por qué Rust permite char y bool como parámetros genéricos constantes pero excluye &str, dado que ambos son técnicamente tipos primitivos?
Char y bool son tipos de valor con tamaños fijos y igualdad estructural trivial; un char es un valor escalar Unicode de 32 bits y bool es estrictamente 0 o 1, lo que permite la comparación bit a bit. &str es un puntero fat (o referencia a un DST) que contiene un puntero de datos y una longitud, introduciendo indireccionamiento y parámetros de vida útil. El compilador no puede garantizar que dos literales de cadena residan en la misma dirección de memoria entre diferentes crates o que sus vidas útiles satisfagan los requisitos de 'static de una manera que permita la verificación de identidad de tipo. En consecuencia, &str carece de las propiedades estructurales requeridas para parámetros genéricos constantes, mientras que char y bool son valores autosuficientes.
¿Cómo podría la implementación de genéricos constantes para tipos de punto flotante romper potencialmente la seguridad de tipos con respecto a los valores de NaN (Not-a-Number)?
Si se permitiera f32, expresiones como MyStructf32::NAN y MyStruct<{ 0.0 / 0.0 }> producirían ambos valores NaN, pero el compilador no podría garantizar que representen el mismo tipo porque NaN != NaN. Esto permitiría la creación de dos monomorfizaciones distintas de lo que debería ser lógicamente el mismo tipo, o, por el contrario, forzar al compilador a fusionar incorrectamente tipos que contienen diferentes payloads de NaN. Esta violación de la identidad de tipo podría llevar a la inconsistencia donde patrones singleton fallan o donde optimizaciones basadas en tipos producen código incorrecto, ya que el compilador asume que los parámetros de tipo identifican de manera única un único tipo.
¿Cuál es la distinción fundamental entre los genéricos constantes y las constantes asociadas, y por qué el primero requiere igualdad estructural mientras que el último no?
Los parámetros genéricos constantes son parte de la identidad del tipo; Container<10> y Container<20> son tipos distintos con monomorfizaciones separadas. Esto requiere que los valores sean comparables en tiempo de compilación para garantizar la unicidad global y fusionar instanciaciones idénticas. Las constantes asociadas son valores asociados con una implementación de tipo pero no alteran el tipo en sí; TypeA y TypeB siguen siendo tipos distintos independientemente de sus valores de constantes asociadas. Por lo tanto, las constantes asociadas pueden ser de tipos de punto flotante o complejos porque simplemente proporcionan valores dentro de la implementación sin afectar la verificación de tipos o la monomorfización, eludiendo la necesidad de igualdad estructural a nivel del sistema de tipos.