Macros en Rust
En la página de sintaxis aprendiste una regla práctica: un ! significa que es una macro, no una función normal: println!, vec!, format!. En esta página, por fin, vemos qué significa eso de verdad.
Por qué println! no puede ser una función normal
Sección titulada «Por qué println! no puede ser una función normal»Una función normal como fn add(a: i32, b: i32) -> i32 siempre recibe un número fijo de argumentos, con tipos fijos. En cambio, puedes llamar a println! con cantidades y tipos de argumentos muy distintos:
fn main() { println!("aquí no hay argumentos"); println!("{}", 42); println!("{} y {}", "dos", 2);}Salida:
aquí no hay argumentos42dos y 2Ninguna firma de función normal podría aceptar todas esas llamadas. Las macros funcionan de otra manera: se ejecutan antes de que tu código se compile del todo, y generan código Rust real según cómo las hayas llamado. println!("{} y {}", "dos", 2) se expande, en la práctica, en código real que maneja exactamente esos dos argumentos: una expansión distinta para cada llamada distinta.
Escribir una macro sencilla
Sección titulada «Escribir una macro sencilla»Puedes definir tus propias macros con macro_rules!. Esta eleva un número al cuadrado:
macro_rules! square { ($x:expr) => { $x * $x };}
fn main() { println!("{}", square!(5));}Salida:
25($x:expr) dice “acepta una expresión y llámala $x”. La parte => { ... } es el código en el que se expande la macro: en cada sitio donde escribas square!(5), el compilador pone 5 * 5 antes de compilar nada.
Una macro que acepta varios argumentos
Sección titulada «Una macro que acepta varios argumentos»Las macros pueden reconocer patrones más complejos, incluida una lista de argumentos que se repite. Esto es algo que una función normal sencillamente no puede hacer:
macro_rules! sum { ($($x:expr),*) => { { let mut total = 0; $(total += $x;)* total } };}
fn main() { println!("{}", sum!(1, 2, 3)); println!("{}", sum!(10, 20));}Salida:
630$($x:expr),* significa “cero o más expresiones, separadas por comas”. Es el mismo patrón que permite que tanto vec![1, 2, 3] como vec![1, 2, 3, 4, 5] funcionen con una sola definición de la macro.
Más de una regla
Sección titulada «Más de una regla»Una macro puede tener varias reglas, separadas por punto y coma. Rust las prueba de arriba abajo y usa la primera que coincide. Una regla puede incluso volver a llamar a la propia macro. Aquí tienes un max! que funciona con cualquier número de argumentos:
macro_rules! max { // Regla 1: un solo valor. El mayor es el propio valor. ($x:expr) => { $x }; // Regla 2: un valor y, después, uno o más valores. ($x:expr, $($rest:expr),+) => { { let first = $x; let biggest_of_rest = max!($($rest),+); if first > biggest_of_rest { first } else { biggest_of_rest } } };}
fn main() { println!("{}", max!(7)); println!("{}", max!(3, 9, 4)); println!("{}", max!(2.5, 1.0));}Salida:
792.5Con max!(3, 9, 4), la regla 1 no coincide (hay más de un valor), así que se usa la regla 2. Esta aparta el 3 y expande max!(9, 4), que a su vez expande max!(4). Esa última llamada coincide con la regla 1 y ahí se detiene todo. El orden importa: Rust siempre prueba primero la regla 1.
Aquí aparecen dos piezas de sintaxis nuevas:
+significa una o más veces, mientras que*(el desum!) significa cero o más. La regla 2 necesita al menos un valor después de la coma, así que la llamada interna amax!(...)siempre tiene algo con lo que trabajar.max!funciona con enteros y con decimales, porque la macro simplemente pega el códigofirst > biggest_of_rest. Una función necesitaría genéricos para conseguir lo mismo.
expr no es lo único que puede aceptar una macro. Estos son otros tipos de fragmento que verás en macros reales:
| Fragmento | Acepta | Ejemplo |
|---|---|---|
expr |
cualquier expresión | 2 + 3, name.len() |
ident |
un nombre | count, Point |
ty |
un tipo | i32, Vec<String> |
literal |
un valor literal | 42, "hi" |
Dónde te vas a encontrar macros
Sección titulada «Dónde te vas a encontrar macros»Vas a usar macros todo el tiempo (println!, vec!, format!, #[derive(Debug)]) sin necesitar nunca escribir una propia. Escribir macros personalizadas es una habilidad de nicho, que usan sobre todo quienes crean bibliotecas y necesitan eliminar código repetitivo de verdad en un proyecto grande. Está bien saber que existen y, más o menos, cómo funcionan, pero no es algo a lo que debas recurrir al principio.
Práctica
Sección titulada «Práctica»1. Imprímelo todo
Sección titulada «1. Imprímelo todo»Escribe una macro print_all! que acepte cualquier número de valores, separados por comas, e imprima cada uno en su propia línea. print_all!("manzana", 42, 3.5) debería imprimir tres líneas. Pista: reconoce $($x:expr),* como hace sum! y repite un println! para cada valor con $( ... )*.
Ver solución
macro_rules! print_all { ($($x:expr),*) => { $(println!("{}", $x);)* };}
fn main() { print_all!("manzana", 42, 3.5);}Salida:
manzana423.5La macro se expande en tres llamadas a println! separadas, una por argumento. Cada valor puede ser de un tipo distinto, algo que una función normal no podría manejar.
2. Tu propio vec!
Sección titulada «2. Tu propio vec!»Escribe my_vec!, que funcione como vec!. my_vec![] devuelve un Vec vacío y my_vec![1, 2, 3] devuelve un Vec con esos elementos. Pista: usa dos reglas. La primera no acepta nada, (). La segunda acepta $($x:expr),+, crea un Vec::new() y mete cada $x en él con push.
Ver solución
macro_rules! my_vec { () => { Vec::new() }; ($($x:expr),+) => { { let mut v = Vec::new(); $(v.push($x);)+ v } };}
fn main() { let empty: Vec<i32> = my_vec![]; let numbers = my_vec![1, 2, 3]; println!("{:?} {:?}", empty, numbers);}Salida:
[] [1, 2, 3]Puedes llamar a cualquier macro con (), [] o {}. Se suelen usar corchetes para las macros que crean listas, como vec!, pero my_vec!(1, 2, 3) también funciona. El Vec vacío necesita el tipo : Vec<i32>, porque sin elementos Rust no puede saber qué va a guardar.
Resumen
Sección titulada «Resumen»- Una macro se expande en código real antes de compilar; por eso
println!puede aceptar una cantidad y un tipo de argumentos distintos en cada llamada. - Un
!después de un nombre siempre indica que estás llamando a una macro, no a una función. macro_rules!te permite definir las tuyas, reconociendo patrones como una sola expresión ($x:expr) o una lista repetida ($($x:expr),*).- Una macro puede tener varias reglas. Rust las prueba de arriba abajo, y una regla puede volver a llamar a la propia macro.
- Escribir macros es una habilidad de nicho: con las que ya trae la biblioteca estándar tienes casi todo lo que vas a necesitar.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»Comprueba lo aprendido
1. ¿Por qué println! no puede escribirse como una función normal?
2. ¿Qué significa $x:expr dentro de un patrón de macro_rules!?
3. ¿Cuándo se expanden las macros en código real?
Puntuación: 0 / 3
Para leer más
Sección titulada «Para leer más»- Macros — El libro de Rust (en inglés): cómo está definido
vec!, además de una introducción a las macros procedurales como#[derive]. macro_rules!— Rust by Example (en inglés): ejemplos cortos y ejecutables de designadores, sobrecarga y repetición.- Macros By Example — The Rust Reference (en inglés): la lista completa de tipos de fragmento como
expr,identytt, y cómo funciona exactamente la coincidencia. - The Little Book of Rust Macros (en inglés): un análisis a fondo, gratuito y ameno, para cuando quieras escribir macros más grandes.