Macros declarativas en Rust: macro_rules!, patrones y repetición

Las macros en Rust son código que genera código en tiempo de compilación. Hay dos familias: las macros declarativas (macro_rules!) que hacen pattern matching sobre tokens, y las macros procedurales que reciben un AST y devuelven otro. Este artículo cubre macro_rules!: la forma más accesible de crear abstracciones que el compilador expande antes de compilar.

Sintaxis básica

macro_rules! saludo {
    // patrón => expansión
    () => {
        println!("Hola, mundo!");
    };
    ($nombre:expr) => {
        println!("Hola, {}", $nombre);
    };
}

fn main() {
    saludo!();              // Hola, mundo!
    saludo!("Rust");        // Hola, Rust
    saludo!("Programador"); // Hola, Programador
}

Tipos de fragmento

Cada $x:tipo captura un tipo específico de token:

macro_rules! demo_tipos {
    ($e:expr)  => { println!("expr: {}", $e) };
    ($i:ident) => { println!("ident: {}", stringify!($i)) };
    ($t:ty)    => { println!("tipo: {}", stringify!($t)) };
    ($b:block) => { let _r = $b; };
    ($l:literal) => { println!("literal: {}", $l) };
}

macro_rules! assert_eq_custom {
    ($left:expr, $right:expr) => {
        if $left != $right {
            panic!(
                "assert_eq fallido:n  izquierda: {:?}n  derecha:   {:?}",
                $left, $right
            );
        }
    };
    ($left:expr, $right:expr, $msg:literal) => {
        if $left != $right {
            panic!("{}: {:?} != {:?}", $msg, $left, $right);
        }
    };
}

fn main() {
    assert_eq_custom!(2 + 2, 4);
    assert_eq_custom!(1 + 1, 2, "suma incorrecta");
}

Repetición con $(...)*

// Crear un Vec con sintaxis de literal
macro_rules! mivec {
    ($($x:expr),*) => {
        {
            let mut v = Vec::new();
            $( v.push($x); )*
            v
        }
    };
    // Con trailing comma
    ($($x:expr),+ ,) => { mivec![$($x),+] };
}

// Constructor de HashMap
macro_rules! mapa {
    ($($k:expr => $v:expr),* $(,)?) => {
        {
            let mut m = std::collections::HashMap::new();
            $( m.insert($k, $v); )*
            m
        }
    };
}

fn main() {
    let v = mivec![1, 2, 3, 4, 5];
    println!("{v:?}"); // [1, 2, 3, 4, 5]

    let m = mapa![
        "uno"  => 1,
        "dos"  => 2,
        "tres" => 3,
    ];
    println!("{m:?}");
}

DSL de rutas HTTP

macro_rules! rutas {
    ($($metodo:ident $ruta:literal => $handler:expr),+ $(,)?) => {
        {
            let mut tabla: Vec<(&str, &str, Box<dyn Fn() -> String>)> = vec![];
            $(
                tabla.push((
                    stringify!($metodo),
                    $ruta,
                    Box::new($handler),
                ));
            )+
            tabla
        }
    };
}

fn main() {
    let rutas = rutas![
        GET  "/api/usuarios"      => || "lista de usuarios".to_string(),
        POST "/api/usuarios"      => || "usuario creado".to_string(),
        GET  "/api/articulos"     => || "lista de artículos".to_string(),
    ];

    for (metodo, ruta, handler) in &rutas {
        println!("{metodo} {ruta} ? {}", handler());
    }
}

Depurar con cargo expand

// Instalar: cargo install cargo-expand
// Ejecutar: cargo expand

// Esto expande toda la macro y muestra el código generado.
// Indispensable cuando la macro no compila o hace algo inesperado.

macro_rules! suma_y_muestra {
    ($a:expr, $b:expr) => {
        let resultado = $a + $b;
        println!("{} + {} = {}", $a, $b, resultado);
    };
}

fn main() {
    suma_y_muestra!(3, 4);
    // cargo expand muestra la expansión completa de esta llamada
}

Errores típicos

// ERROR: usar $x antes de capturarlo
macro_rules! malo {
    () => { println!("{}", $x) }; // $x no existe en este brazo
}

// ERROR: tipo de fragmento incorrecto
macro_rules! necesita_ident {
    ($i:ident) => { let $i = 42; };
}
// necesita_ident!(3 + 5); // ERROR: 3+5 es expr, no ident
// necesita_ident!(mi_var); // OK

// AVISO: las macros son higiénicas
// Las variables creadas dentro no filtran al scope exterior
macro_rules! crea_var {
    ($nombre:ident) => {
        let $nombre = 99;
    };
}
fn main() {
    crea_var!(x);
    println!("{x}"); // 99: funciona porque $nombre es ident del caller
}

Resumen

  • macro_rules! nombre { (patrón) => { expansión }; } define una macro declarativa.
  • Los fragmentos más usados: $x:expr, $i:ident, $t:ty, $l:literal.
  • $($x:expr),* captura cero o más expresiones separadas por coma; usa + para una o más.
  • $(,)? al final acepta una coma trailing opcional.
  • Las macros son higiénicas: las variables internas no contaminan el scope del caller.
  • cargo expand muestra el código generado, esencial para depurar.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP