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:

  1. En el primer brazo, se utiliza $nombre:ident para capturar el identificador crear_vec. El parámetro $tipo:ty captura el tipo i32, el cual se utiliza para definir el tipo de retorno Vec<$tipo> y el parámetro inicial. El patrón $( $valor:expr ),* utiliza el cuantificador * para capturar múltiples expresiones (10, 20 y 30) separadas por comas. Dentro de la expansión, se utiliza un bloque $( ... )* para repetir la llamada a lista.push($valor); por cada elemento capturado.
  2. En el segundo brazo, $nombre:ident captura el identificador ejecutar_bloque, y $bloque:block captura 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ón ejecutar_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.

99