Ir al contenido

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 argumentos
42
dos y 2
▶ Pruébalo tú mismo

Ninguna 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.

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
▶ Pruébalo tú mismo

($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.

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:

6
30
▶ Pruébalo tú mismo

$($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.

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:

7
9
2.5
▶ Pruébalo tú mismo

Con 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 de sum!) significa cero o más. La regla 2 necesita al menos un valor después de la coma, así que la llamada interna a max!(...) siempre tiene algo con lo que trabajar.
  • max! funciona con enteros y con decimales, porque la macro simplemente pega el código first > 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"

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.

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:

manzana
42
3.5
▶ Pruébalo tú mismo

La 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.

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]
▶ Pruébalo tú mismo

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.

  • 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 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