Las macros declarativas en Rust, implementadas mediante la palabra clave macro_rules!, son herramientas de metaprogramación que permiten la expansión de código mediante el emparejamiento de patrones (pattern matching). A diferencia de las macros de procedimiento, que actúan sobre flujos de tokens mediante funciones complejas, las macros declarativas operan sobre la sintaxis del lenguaje mediante reglas predefinidas. Esto las hace ideales para eliminar el código repetitivo (boilerplate) y crear pequeñas lenguas específicas (DSLs) dentro de Rust.
La estructura de una macro declarativa se compone de uno o varios “brazos” (arms). Cada brazo contiene un patrón que el compilador intenta emparejar con los tokens proporcionados en la invocación. El proceso es secuencial: el compilador prueba los patrones en el orden en que están escritos y ejecuta el primero que resulte exitoso. Un error común es definir patrones demasiado genéricos al principio de la macro, lo que puede causar que un patrón más específico sea ignorado (el “devoramiento” de tokens), resultando en errores de compilación difíciles de depurar.
Para capturar partes del código, se utilizan los fragment specifiers, que definen qué tipo de sintaxis se espera:
– $ident: Un identificador (nombre de variable, función o tipo).
– $expr: Una expresión válida (como a + b).
– $ty: Un tipo de dato (como i32 o Vec<u8>).
– $stmt: Una sentencia (como let x = 5;).
– $pat: Un patrón de coincidencia (usado en estructuras match).
– $block: Un bloque de código delimitado por llaves { ... }.
– $tt (Token Tree): El fragmento más flexible, que representa un único token o un grupo de tokens.
Para manejar la repetición de elementos, se utiliza la sintaxis $( ... )<cuantificador>, donde el cuantificador puede ser * (cero o más), + (uno o más) o ? (cero o uno).
macro_rules! generador_de_datos {
// Brazo 1: Captura un identificador, un tipo y una lista de expresiones
($nombre:ident, $tipo:ty, $($valor:expr),*) => {
fn $nombre(inicial: $tipo) -> Vec<$tipo> {
// Creamos un vector con el valor inicial
let mut lista = vec![inicial];
// Iteramos sobre las expresiones capturadas mediante repetición
$(
lista.push($valor);
)*
lista
}
};
// Brazo 2: Captura un identificador y un bloque de código
($nombre:ident, $bloque:block) => {
fn $nombre() {
// Se expande el bloque directamente en el cuerpo de la función
$bloque
}
};
}
fn main() {
// Invocación con el primer brazo: usa repetición de expresiones
// Crea la función 'crear_vec' que retorna un Vec<i32>
let mi_vector = generador_de_datos!(crear_vec, i32, 10, 20, 30);
println!("Vector generado: {:?}", mi_vector);
// Invocación con el segundo brazo: usa un bloque de código
// Crea la función 'ejecutar_bloque' con el código proporcionado
generador_de_datos!(ejecutar_bloque, {
let x = 5;
println!("Resultado del bloque: {}", x * 2);
});
}
Explicación del Código
En el código anterior, la macro generador_de_datos! presenta dos brazos de expansión:
- En el primer brazo, se utiliza
$nombre:identpara capturar el identificadorcrear_vec. El parámetro$tipo:tycaptura el tipoi32, el cual se utiliza para definir el tipo de retornoVec<$tipo>y el parámetroinicial. El patrón$( $valor:expr ),*utiliza el cuantificador*para capturar múltiples expresiones (10,20y30) separadas por comas. Dentro de la expansión, se utiliza un bloque$( ... )*para repetir la llamada alista.push($valor);por cada elemento capturado. - En el segundo brazo,
$nombre:identcaptura el identificadorejecutar_bloque, y$bloque:blockcaptura todo el bloque de código delimitado por llaves{ ... }. Durante la expansión, el bloque se inserta directamente como el cuerpo de la nueva funciónejecutar_bloque.
En la función main, la invocación generador_de_datos!(crear_vec, i32, 10, 20, 30) dispara la expansión del primer brazo, resultando en la definición de una función que inicializa un Vec<i32>. La segunda invocación generador_de_datos!(ejecutar_bloque, { ... }) utiliza el segundo brazo para generar una función que ejecuta el código lógico interno.
N° 99