Las macros declarativas en Rust son potentes herramientas de metaprogramación que permiten expandir código basándose en patrones de tokens. A diferencia de las macros procedurales, macro_rules! opera mediante un sistema de emparejamiento de patrones (pattern matching) sobre tokens. Para lograr complejidad avanzada, es esencial dominar tres pilares: las repeticiones, la recursión mediante reglas internas y el uso de $crate.

Repeticiones y Delimitadores

La sintaxis de repetición $( ... )* (cero o más), $( ... )+ (uno o más) o $( ... )? (cero o uno) permite iterar sobre fragmentos de código. Es crucial distinguir entre el patrón de repetición y el delimitador. Por ejemplo, $($x:expr),* significa “toma múltiples expresiones separadas por comas”. El delimitador (la coma en este caso) es procesado por el compilador para identificar dónde termina cada iteración, permitiendo que la macro sea flexible ante listas de argumentos.

El uso de $crate

Cuando desarrollas una biblioteca (crate) y exportas una macro mediante #[macro_export], la macro deja de pertenecer a tu módulo y se mueve a la raíz del crate del usuario que la utilice. Si tu macro hace referencia a un tipo o trait definido en tu propia librería (ej. MiTrait), el compilador fallará al buscar MiTrait en el contexto del usuario. Para solucionar esto, se usa $crate. Esta palabra clave se expande exactamente a la ruta raíz de la crate donde la macro fue definida, garantizando que las referencias internas siempre apunten a los tipos correctos, independientemente de dónde se invoque la macro.

Recursión y Máquinas de Estado

macro_rules! no puede llamar a una función de forma recursiva como una función normal, pero puede expandirse a sí misma mediante “reglas internas”. Un patrón avanzado consiste en definir una macro con una regla principal y varias reglas auxiliares (comúnmente usando un prefijo como @ para evitar colisiones). Esto permite procesar listas de tokens de forma iterativa, comportándose como una máquina de estados que descompone una lista en “cabeza” y “cola” hasta llegar a un caso base.

// Definición de un trait en una crate hipotética
pub trait Identificable {
    fn identificar(&self);
}

// Implementación de un struct para probar la macro
struct Persona { nombre: String }
struct Robot { id: u32 }

// Macro avanzada: Implementa Identificable para una lista de tipos
// Usamos #[macro_export] para simular una macro de biblioteca
#[macro_export]
macro_rules! implementa_identificadores {
    // Regla principal: recibe una lista de tipos separados por comas
    ($($t:ty),*) => {
        // Inicia la recursión usando una regla interna con prefijo '@'
        $crate::implementa_identificadores!(@paso $($t),*);
    };

    // Caso base: Solo queda un tipo en la lista
    (@paso $t:ty) => {
        impl $crate::Identificable for $t {
            fn identificar(&self) {
                println!("Soy un tipo: {}", std::any::type_name::<$t>());
            }
        }
    };

    // Regla recursiva: procesa el primer tipo y llama a la macro para el resto
    (@paso $t:ty, $($resto:ty),*) => {
        impl $crate::Identificable for $t {
            fn identificar(&self) {
                println!("Soy un tipo: {}", std::any::type_name::<$t>());
            }
        }
        // Llamada recursiva para procesar el resto de los tipos
        $crate::implementa_identificadores!(@paso $($resto),*);
    };
}

// Nota: Para que este ejemplo compile en un solo archivo, 
// simulamos el comportamiento de una macro exportada.
impl Identificable for Persona {
    fn identificar(&self) { println!("Persona"); }
}
impl Identificable for Robot {
    fn identificar(&self) { println!("Robot"); }
}

fn main() {
    // La macro se expande para generar implementaciones individuales
    // de Identificable para Persona y Robot.
    implementa_identificadores!(Persona, Robot);

    let p = Persona { nombre: "Alice".into() };
    let r = Robot { id: 1 };

    p.identificar();
    r.identificar();
}

Análisis del Código

  1. $($t:ty),*: En la regla principal, esto captura una secuencia de tipos ($t:ty) separados por comas. La macro toma toda la lista y la pasa a la fase de recursión.
  2. @paso: Este es un prefijo arbitrario utilizado para crear reglas internas. Permite que la macro distinga entre una llamada “limpia” del usuario y una llamada interna de expansión.
  3. $crate::Identificable: El uso de $crate asegura que, aunque la macro sea exportada, siempre buscará el trait Identificable en la crate original donde se definió, evitando errores de “tipo no encontrado” en el crate del cliente.
  4. Caso Base (@paso $t:ty): Es la condición de parada de la recursión. Cuando la lista de tipos se reduce a un solo elemento, se implementa el trait para ese elemento y no se vuelve a llamar a la macro.
  5. Caso Recursivo (@paso $t:ty, $($resto:ty),*): Este es el núcleo de la lógica. Divide la lista en el elemento actual ($t) y el resto de la lista ($resto). Expande la implementación para el elemento actual y luego invoca recursivamente la macro con la lista restante.

100