Pular para o conteúdo

Macros em Rust

Lá na página de Sintaxe, você aprendeu a regra prática: um ! indica que aquilo é uma macro, não uma função comum: println!, vec!, format!. Esta página finalmente explica o que isso quer dizer.

Uma função comum, como fn add(a: i32, b: i32) -> i32, sempre recebe um número fixo de argumentos, de tipos fixos. Mas você pode chamar println! com quantidades e tipos de argumentos totalmente diferentes:

fn main() {
println!("nenhum argumento aqui");
println!("{}", 42);
println!("{} e {}", "dois", 2);
}

Saída:

nenhum argumento aqui
42
dois e 2
▶ Experimente você mesmo

Nenhuma assinatura de função comum conseguiria aceitar todas essas chamadas. As macros funcionam de outro jeito: elas rodam antes de o seu código ser compilado de vez, gerando código Rust de verdade de acordo com a forma como foram chamadas. Na prática, println!("{} e {}", "dois", 2) se expande em um código que trata exatamente esses dois argumentos, com uma expansão diferente para cada chamada diferente.

Você pode definir as suas próprias macros com macro_rules!. Esta aqui eleva um número ao quadrado:

macro_rules! square {
($x:expr) => {
$x * $x
};
}
fn main() {
println!("{}", square!(5));
}

Saída:

25
▶ Experimente você mesmo

($x:expr) quer dizer “aceite uma expressão e chame-a de $x”. A parte => { ... } é o código em que a macro se expande: em todo lugar onde você escreve square!(5), o compilador coloca 5 * 5 no lugar antes de compilar qualquer coisa.

As macros conseguem reconhecer padrões mais complexos, incluindo uma lista de argumentos que se repete. Isso é algo que uma função comum simplesmente não consegue fazer:

macro_rules! sum {
($($x:expr),*) => {
{
let mut total = 0;
$(total += $x;)*
total
}
};
}
fn main() {
println!("{}", sum!(1, 2, 3));
println!("{}", sum!(10, 20));
}

Saída:

6
30
▶ Experimente você mesmo

$($x:expr),* quer dizer “zero ou mais expressões, separadas por vírgulas”. É o mesmo padrão que faz vec![1, 2, 3] e vec![1, 2, 3, 4, 5] funcionarem com uma única definição de macro.

Uma macro pode ter várias regras, separadas por ponto e vírgula. O Rust tenta cada uma de cima para baixo e usa a primeira que combinar. Uma regra pode até chamar a própria macro de novo. Veja um max! que funciona com qualquer quantidade de argumentos:

macro_rules! max {
// Regra 1: um único valor. O maior é o próprio valor.
($x:expr) => {
$x
};
// Regra 2: um valor e, depois, um ou mais outros.
($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));
}

Saída:

7
9
2.5
▶ Experimente você mesmo

Em max!(3, 9, 4), a regra 1 não combina (há mais de um valor), então quem combina é a regra 2. Ela guarda o 3 e expande max!(9, 4), que por sua vez expande max!(4). Essa última combina com a regra 1, e a expansão para ali. A ordem importa: o Rust sempre tenta a regra 1 primeiro.

Aqui aparecem duas novidades pequenas de sintaxe:

  • + quer dizer um ou mais, enquanto * (do sum!) quer dizer zero ou mais. A regra 2 precisa de pelo menos um valor depois da vírgula, então a chamada interna max!(...) sempre tem algo com que trabalhar.
  • max! funciona com inteiros e com números de ponto flutuante, porque a macro só cola o código first > biggest_of_rest no lugar. Uma função precisaria de genéricos para fazer isso.

expr não é o único tipo de coisa que uma macro pode aceitar. Alguns outros que você vai ver em macros de verdade:

Fragmento Combina com Exemplo
expr qualquer expressão 2 + 3, name.len()
ident um nome count, Point
ty um tipo i32, Vec<String>
literal um valor literal 42, "hi"

Você vai usar macros o tempo todo (println!, vec!, format!, #[derive(Debug)]) sem nunca precisar escrever uma. Criar macros próprias é uma habilidade de nicho, usada principalmente por autores de bibliotecas que precisam eliminar código repetitivo (boilerplate) em um projeto grande. Vale saber que elas existem e mais ou menos como funcionam, mas não é algo para usar logo no começo.

Escreva uma macro print_all! que receba qualquer quantidade de valores, separados por vírgulas, e imprima cada um em uma linha. print_all!("maçã", 42, 3.5) deve imprimir três linhas. Dica: use o padrão $($x:expr),*, como o sum!, e repita um println! para cada valor com $( ... )*.

Ver solução
macro_rules! print_all {
($($x:expr),*) => {
$(println!("{}", $x);)*
};
}
fn main() {
print_all!("maçã", 42, 3.5);
}

Saída:

maçã
42
3.5
▶ Experimente você mesmo

A macro se expande em três chamadas separadas de println!, uma para cada argumento. Cada valor pode ter um tipo diferente, coisa que uma função comum não conseguiria aceitar.

Escreva uma macro my_vec! que funcione como o vec!. my_vec![] gera um Vec vazio, e my_vec![1, 2, 3] gera um Vec com esses itens. Dica: use duas regras. A primeira não combina com nada, (). A segunda combina com $($x:expr),+, cria um Vec::new() e coloca cada $x nele com push.

Ver solução
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);
}

Saída:

[] [1, 2, 3]
▶ Experimente você mesmo

Dá para chamar qualquer macro com (), [] ou {}. Por costume, usamos colchetes em macros de lista, como o vec!, mas my_vec!(1, 2, 3) também funciona. O Vec vazio precisa do tipo : Vec<i32>, porque, sem nenhum item, o Rust não tem como saber o que ele vai guardar.

  • Uma macro se expande em código de verdade antes da compilação; é assim que println! consegue aceitar uma quantidade e um tipo de argumentos diferentes a cada chamada.
  • Um ! depois de um nome é sempre o sinal de que você está chamando uma macro, e não uma função.
  • macro_rules! permite definir as suas próprias macros, reconhecendo padrões como uma única expressão ($x:expr) ou uma lista que se repete ($($x:expr),*).
  • Uma macro pode ter várias regras. O Rust tenta cada uma de cima para baixo, e uma regra pode chamar a própria macro de novo.
  • Escrever macros é uma habilidade de nicho: usar as que já vêm na biblioteca padrão cobre quase tudo de que você vai precisar.

Teste o que aprendeu

1. Por que println! não pode ser escrita como uma função comum?

2. O que $x:expr quer dizer dentro de um padrão de macro_rules!?

3. Quando as macros se expandem em código de verdade?

Pontuação: 0 / 3