wandres.dev
MACROS DECLARATIVAS · macro_rules!

Casos reales: reimplementar vec!, un mini-DSL y cuándo usar macros

Junta todo lo aprendido: las tres reglas reales de vec!, un mini-DSL que genera un enum y su impl —algo imposible para una función— y el marco de decisión definitivo. Cuándo una macro declarativa es la herramienta correcta frente a una función, a los genéricos o a una macro procedural.

⏱ 22 min

Ya tienes las piezas: patrones sobre tokens, especificadores de fragmento, repetición e higiene. Toca ensamblarlas en código que de verdad escribirías. Reconstruiremos vec! tal como es en realidad —con sus tres reglas—, escribiremos un mini-DSL que genera un enum entero junto a su impl, algo que ninguna función podría hacer, y cerraremos con el juicio más importante de todos: cuándo una macro declarativa es la herramienta correcta, y cuándo estás usando un martillo para algo que pedía un destornillador. Porque la maestría con las macros no consiste en escribir muchas, sino en saber reconocer las pocas ocasiones en que son la respuesta.

🎯 Al terminar esta lección sabrás
  • Implementar vec! fielmente con sus tres reglas: vacío, repetición y lista.
  • Escribir un mini-DSL que genere ítems —un enum y su impl— con macro_rules!.
  • Distinguir cuándo procede una función, los genéricos, una macro declarativa o una procedural.
  • Interiorizar los límites de macro_rules! para no forzarla más allá de su terreno.

vec! por dentro: las tres reglas

El vec! de la biblioteca estándar no es una macro monolítica: son tres reglas que cubren tres formas de invocarlo. La versión de enseñanza, fiel en estructura a la real, es esta:

macro_rules! vector {
    // Regla 1: sin argumentos, un Vec vacio.
    () => {
        Vec::new()
    };
    // Regla 2: un elemento repetido n veces, como vec![0; 5].
    ($elem:expr; $n:expr) => {
        ::std::vec::from_elem($elem, $n)
    };
    // Regla 3: una lista de elementos, con coma final opcional.
    ($( $x:expr ),+ $(,)?) => {{
        let mut v = Vec::new();
        $( v.push($x); )+
        v
    }};
}

fn main() {
    let a: Vec<i32> = vector![];        // regla 1
    let b = vector![7; 3];              // regla 2  ->  [7, 7, 7]
    let c = vector![1, 2, 3];           // regla 3
    assert_eq!((a.len(), b, c), (0, vec![7, 7, 7], vec![1, 2, 3]));
}

Observa la selección de regla por forma sintáctica. La 1 no tiene tokens; la 2 se distingue por el punto y coma que separa elemento y cuenta; la 3 empareja una lista separada por comas con + —una o más— y su $(,)? para la coma final. El compilador prueba de arriba abajo y elige la primera que cuadra. Ese despacho por sintaxis, imposible en una función, es justo lo que hace de vec! una macro. La real delega en from_elem y en primitivas internas para la repetición eficiente, pero el esqueleto es exactamente este.

Un mini-DSL: generar código, no solo valores

El verdadero superpoder aparece cuando una macro genera ítems —definiciones enteras—, no meras expresiones. Aquí ninguna función alcanza: una función produce valores en ejecución, jamás un enum o un impl nuevos. Este pequeño DSL declara un enum y le adosa un método que devuelve el nombre de cada variante como texto:

macro_rules! enum_nombrado {
    ($nombre:ident { $( $variante:ident ),+ $(,)? }) => {
        #[derive(Debug, Clone, Copy, PartialEq)]
        enum $nombre {
            $( $variante ),+
        }

        impl $nombre {
            fn nombre(&self) -> &'static str {
                match self {
                    $( $nombre::$variante => stringify!($variante), )+
                }
            }

            fn todas() -> &'static [$nombre] {
                &[ $( $nombre::$variante ),+ ]
            }
        }
    };
}

enum_nombrado!(Color { Rojo, Verde, Azul });

fn main() {
    assert_eq!(Color::Verde.nombre(), "Verde");
    assert_eq!(Color::todas().len(), 3);
}

Una sola invocación generó un enum con tres variantes, un impl con un match exhaustivo sobre ellas y un inventario de todas. La macro stringify! convierte el token del identificador en su texto, y la repetición $()+ teje a la vez las variantes, los brazos del match y los elementos del array. Escribir esto a mano para diez enums sería repetición pura; con la macro, la fuente de verdad es la lista de variantes y todo lo demás se deriva. Ese es el nicho donde una macro declarativa brilla: código estructuralmente idéntico que solo cambia en los nombres.

⚠️
No abuses: una macro que genera ítems es difícil de leer y de depurar

El poder de generar ítems tiene un coste. El código que sale de la macro no aparece en tu editor, los mensajes de error pueden señalar la expansión en vez de tu invocación, el salto a definición del IDE se pierde y quien lea el crate tendrá que expandir mentalmente la macro para entenderlo. Usa este recurso solo cuando la repetición que elimina sea real y sustancial. Para dos o tres casos, escribir el código a mano casi siempre gana en claridad. La macro se justifica cuando la lista de casos es larga, cambia a menudo o debe mantenerse sincronizada sin fisuras.

Cuándo una macro es la herramienta correcta

Aquí está el juicio que separa al que conoce las macros del que las domina. Ordena tus herramientas de la más simple a la más pesada y baja solo cuando la anterior no llega:

🔧

Función — por defecto

Si abstraes comportamiento sobre valores con una firma fija, usa una función. Es legible, se comprueba de tipos, se compone y no infla el tiempo de compilación. La opción por defecto, siempre.

🧬

Genéricos y traits — sobre tipos

Si necesitas el mismo comportamiento para muchos tipos, usa genéricos con cotas de trait. Sigue siendo código normal, verificado y con buenos errores; no toques macros para esto.

🪄

macro_rules! — sobre sintaxis

Interfaz variádica como vec!, generación de ítems, o aceptar sintaxis que no es una expresión válida. Cuando la transformación se expresa como patrón hacia plantilla, es su terreno.

⚙️

Macro procedural — inspeccionar tokens

Si necesitas analizar los tokens, derivar de los campos de un struct (#[derive]) o hacer codegen con lógica arbitraria, sube a una macro procedural: una función Rust sobre TokenStream, en su propio crate.

La frontera decisiva está entre las dos últimas. macro_rules! es un emparejador de patrones: reescribe según plantillas, pero no computa. No sabe hacer aritmética de verdad, no puede recorrer los campos de un tipo, no toma decisiones basadas en el contenido de un fragmento —solo en su forma—, y su recursión tiene un límite. En cuanto necesitas razonar sobre el código en vez de estamparlo —inspeccionar los campos de un struct para un derive, validar contra un esquema, generar a partir de un fichero externo—, macro_rules! se queda corta y toca una macro procedural, que es código Rust ejecutándose en compilación sobre los tokens como datos. La regla final: usa una función si puedes, una macro declarativa si la interfaz lo exige, y una procedural solo cuando de verdad haga falta programar la generación.

flowchart TD
start[Que necesitas abstraer] --> val[Comportamiento sobre valores]
val -->|si| fn[Usa una funcion]
val -->|no| ty[Lo mismo para muchos tipos]
ty -->|si| gen[Usa genericos y traits]
ty -->|no| syn[Variadica o generar items o sintaxis]
syn -->|si| decl[Basta emparejar patrones]
syn -->|no| fn
decl -->|si| mr[Usa macro_rules]
decl -->|no, hay que inspeccionar tokens| proc[Usa una macro procedural]
style fn fill:#a6e3a1,color:#11111b
style mr fill:#cba6f7,color:#11111b
style proc fill:#89b4fa,color:#11111b
La madurez con las macros es saber cuándo no escribirlas

Hay una ironía en el aprendizaje de las macros que conviene nombrar. Cuanto mejor las entiendes, menos las usas, y esa contención es precisamente la señal de que las has entendido. Una macro es una abstracción que opera sobre la sintaxis, y toda abstracción que opera sobre la sintaxis paga un impuesto: el código deja de ser legible tal cual, las herramientas —el salto a definición, el autocompletado, los mensajes de error— pierden pie, y el siguiente que lea el crate, que puede ser tú dentro de un año, tendrá que expandir la macro en su cabeza para saber qué corre en realidad. Ese impuesto está justificado cuando compra algo que ninguna otra herramienta ofrece: una interfaz variádica que una firma de función no puede expresar, la generación de una familia entera de ítems a partir de una lista, la aceptación de una sintaxis que no es una expresión de Rust. Fuera de esos casos, casi siempre había una respuesta más humilde y mejor. Una función, primero: si tu macro no genera ítems ni es variádica ni acepta sintaxis rara, es una función disfrazada, y disfrazarla solo la vuelve más difícil de leer y de comprobar. Los genéricos, después: si lo que varía es el tipo y no la forma sintáctica, el sistema de tipos ya te da abstracción sin coste y con errores decentes. Y cuando de verdad necesites metaprogramación pesada —inspeccionar campos, derivar sobre estructuras, generar desde datos externos—, reconoce que macro_rules! es un emparejador de plantillas, no un lenguaje de programación, y sube a una macro procedural en lugar de retorcer la declarativa hasta lo ilegible. El buen ingeniero de Rust no colecciona macros como trofeos; alcanza la más simple que resuelve el problema y guarda la artillería para las pocas veces que el problema la pide de verdad. Saber escribir una macro es habilidad; saber cuándo no escribirla es criterio, y el criterio es lo que perdura.

⚔️ Ensambla y juzga
  1. Escribe vector! con las tres reglas y prueba las tres formas de invocarla; explica qué token distingue a cada regla y por qué el orden importa.
  2. Amplía enum_nombrado! para que además genere un método desde_nombre que haga el camino inverso, de texto a variante devolviendo un Option.
  3. Toma una macro trivial que solo sume dos expresiones y reescríbela como función; argumenta por qué la función es preferible aquí.
  4. Identifica en un caso concreto la línea donde macro_rules! ya no basta y haría falta una macro procedural; describe qué necesitarías inspeccionar de los tokens.
  5. Recorre el árbol de decisión con tres necesidades reales tuyas —una de valores, una de tipos, una de sintaxis— y justifica en cada una qué herramienta elegirías.