Repetición: generar código para listas variádicas
El operador de repetición $()* , $()+ y $()? convierte macro_rules! en variádica: empareja una lista de longitud arbitraria y expande código una vez por elemento. Es el mecanismo detrás de vec![1, 2, 3], de println! y de todo constructor que acepte cuantos argumentos quieras. Separadores, coma final y repetición anidada.
Hasta ahora tus macros aceptaban un número fijo de fragmentos. Pero la gracia de vec![1, 2, 3], de println! o de cualquier constructor cómodo es aceptar cuantos argumentos quieras. Esa capacidad variádica nace de un solo mecanismo: el operador de repetición. Con $( ... )* una macro empareja una lista de longitud arbitraria, y con el mismo $( ... )* en la expansión genera código una vez por cada elemento capturado. Es, a la vez, un bucle sobre la sintaxis de entrada y una plantilla que se estampa N veces en la salida. Comprenderlo es pasar de escribir macros de juguete a escribir las que de verdad usarías.
- Leer y escribir la sintaxis de repetición
$( ... )sep opcon*,+y?. - Emparejar listas variádicas separadas por comas u otros tokens.
- Reconstruir un constructor tipo
vec!con un bucle depushgenerado. - Manejar la coma final opcional, la repetición anidada y el conteo de elementos.
La sintaxis de la repetición
Un grupo de repetición tiene tres partes: el patrón repetido entre $( ... ), un token separador opcional, y un operador que dice cuántas veces. Los tres operadores son:
*— cero o más repeticiones.+— una o más repeticiones.?— cero o una (sin separador, porque no hay nada que separar de una sola cosa).
Así, $( $x:expr ),* significa “cero o más expresiones separadas por comas”, y $( $x:expr );+ significa “una o más expresiones separadas por punto y coma”. En la expansión, se vuelve a envolver el código repetido con $( ... )* y el compilador itera: por cada elemento que capturó en la entrada, estampa una copia de la plantilla, sustituyendo $x por el valor de esa iteración. El número de repeticiones de salida queda atado al de entrada.
macro_rules! imprime_todos {
( $( $x:expr ),* ) => {
$(
println!("{}", $x);
)*
};
}
imprime_todos!(1, "dos", 3.0); // genera tres println!, uno por argumento
La lectura es directa: el patrón $( $x:expr ),* capturó tres expresiones; el bloque $( println!(...); )* se estampa tres veces, una con cada una. Si invocas imprime_todos!() sin argumentos, el * admite cero repeticiones y no se genera nada.
Reconstruir vec! con un bucle de push
El ejemplo canónico. Queremos que vector![10, 20, 30] construya un Vec con esos tres elementos. La estrategia clásica: abrir un bloque, crear un Vec vacío, generar un push por elemento y devolver el vector.
macro_rules! vector {
( $( $x:expr ),* ) => {{
let mut v = Vec::new();
$(
v.push($x);
)*
v
}};
}
fn main() {
let nums = vector![10, 20, 30];
assert_eq!(nums, vec![10, 20, 30]);
let vacio: Vec<i32> = vector![];
assert!(vacio.is_empty());
}
De nuevo la llave doble {{ }}: la macro expande a una expresión de bloque que declara v, ejecuta los push generados y evalúa a v. Como el * acepta cero elementos, vector![] funciona y produce un Vec vacío. Ya tienes, en ocho líneas, el esqueleto de la macro más famosa de Rust.
Separador, coma final y repetición anidada
Tres refinamientos elevan una macro de repetición de juguete a producción.
La coma final. El patrón $( $x:expr ),* no admite una coma sobrante: vector![10, 20,] falla, porque tras el último elemento espera otro elemento, no el final. Como vec! sí tolera la coma final, la técnica es añadir un grupo opcional al terminar: $( $x:expr ),* $(,)?, que dice “y quizá una coma más”.
macro_rules! vector {
( $( $x:expr ),* $(,)? ) => {{
let mut v = Vec::new();
$( v.push($x); )*
v
}};
}
let ok = vector![10, 20, 30,]; // ahora la coma final se acepta
Repetición anidada. Las repeticiones se pueden anidar, y la profundidad con que usas una metavariable en la expansión debe coincidir con la profundidad con que la capturaste. Un constructor de HashMap combina un separador => dentro de cada par y comas entre pares:
macro_rules! mapa {
( $( $clave:expr => $valor:expr ),* $(,)? ) => {{
let mut m = std::collections::HashMap::new();
$( m.insert($clave, $valor); )*
m
}};
}
let edades = mapa!{ "ana" => 30, "leo" => 25 };
Contar elementos. macro_rules! no sabe hacer aritmética, pero hay un idioma para contar repeticiones y, por ejemplo, pre-reservar capacidad. Se suma un 1 por elemento, descartando el valor:
macro_rules! cuenta {
( $( $x:expr ),* $(,)? ) => {
0usize $( + { let _ = &$x; 1 } )*
};
}
const N: usize = cuenta!(a, b, c); // expande a 0usize + 1 + 1 + 1 == 3
Con eso, un vector! afinado haría Vec::with_capacity(cuenta!($($x),*)) antes de los push, evitando realojos. Es el tipo de truco que distingue una macro escrita con oficio.
flowchart TD inv[Invocacion vector 10 coma 20 coma 30] --> cap[El patron captura tres expresiones en x] cap --> rep[El bloque de expansion se estampa tres veces] rep --> p1[v.push de 10] rep --> p2[v.push de 20] rep --> p3[v.push de 30] p1 --> out[Un Vec con tres elementos] p2 --> out p3 --> out style inv fill:#cba6f7,color:#11111b style rep fill:#89b4fa,color:#11111b style out fill:#a6e3a1,color:#11111b
Una confusión habitual: en $( ... ),* la coma es el separador, es decir, va entre repeticiones, nunca tras la última. Por eso la coma final necesita su propio $(,)? aparte. Y ojo: el separador puede ser casi cualquier token único —;, =>, |—, pero solo uno; no puedes separar por dos tokens. Si necesitas algo más rico como delimitador, replantea el patrón o recurre a tt y a la recursión.
Detente a considerar lo que ocurre en $( v.push($x); )*. No es un bucle en el sentido corriente: cuando el programa arranca, ya no hay ninguna iteración que resolver, porque las tres llamadas a push están ahí escritas, una tras otra, como si las hubieras tecleado a mano. El bucle se ejecutó en la compilación, sobre la sintaxis, y su resultado fue código plano. Esta es la esencia de la metaprogramación por repetición y merece asombro: describes una acción una sola vez, sobre un elemento representativo, y el compilador la despliega tantas veces como argumentos reciba, cosiendo el resultado en el árbol de sintaxis antes de que el optimizador siquiera empiece. De ahí sale toda la ergonomía variádica de Rust. vec!, println!, hashmap!, cualquier constructor que acepte una lista de longitud libre, no son primitivas mágicas del lenguaje: son este mismo patrón —capturar con $()*, estampar con $()*— aplicado con gusto. Y observa la disciplina que impone: la profundidad de repetición en la salida debe casar con la de la entrada, porque la macro no inventa iteraciones, las reparte; cada $x de la plantilla se enlaza con exactamente un $x capturado, en orden. Es un bucle puro, sin estado ni condición, que solo sabe recorrer en paralelo las listas que emparejó. Esa pureza es lo que lo hace predecible y lo que permite anidarlo: una lista de listas se despliega en dos niveles de repetición encajados sin más ceremonia. Cuando entiendes que $()* es a la sintaxis lo que un for es a los valores, dejas de ver las macros variádicas como excepciones del lenguaje y empiezas a verlas como lo que son: pequeños programas que corren en el compilador y cuya salida es tu programa.
- Escribe
vector!conpush, añádele el$(,)?para la coma final y comprueba quevector![1, 2, 3,]compila mientras que sin el$(,)?no. - Implementa
mapa!paraHashMapcon=>como separador de par y úsalo; luego añade la coma final opcional entre pares. - Escribe la macro
cuenta!y verifica con unaconstquecuenta!(a, b, c)vale tres; explica por quémacro_rules!necesita este rodeo en vez de sumar directamente. - Combina lo anterior: haz que
vector!cuente sus elementos y llame aVec::with_capacityantes de lospush; razona qué ganas. - Diseña una macro de repetición anidada, por ejemplo
matriz![[1, 2], [3, 4]], y explica cómo la profundidad de$()*en la expansión refleja la de la captura.