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.
Por que println! não pode ser uma função comum
Seção intitulada “Por que println! não pode ser uma função comum”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 aqui42dois e 2Nenhuma 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.
Escrevendo uma macro simples
Seção intitulada “Escrevendo uma macro simples”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($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.
Uma macro que aceita vários argumentos
Seção intitulada “Uma macro que aceita vários argumentos”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:
630$($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.
Mais de uma regra
Seção intitulada “Mais de uma regra”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:
792.5Em 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*(dosum!) quer dizer zero ou mais. A regra 2 precisa de pelo menos um valor depois da vírgula, então a chamada internamax!(...)sempre tem algo com que trabalhar.max!funciona com inteiros e com números de ponto flutuante, porque a macro só cola o códigofirst > biggest_of_restno 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" |
Onde você vai encontrar macros de verdade
Seção intitulada “Onde você vai encontrar macros de verdade”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.
Prática
Seção intitulada “Prática”1. Imprimir tudo
Seção intitulada “1. Imprimir tudo”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çã423.5A 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.
2. O seu próprio vec!
Seção intitulada “2. O seu próprio vec!”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]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
Seção intitulada “Teste o que aprendeu”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
Para saber mais
Seção intitulada “Para saber mais”- Macros — The Rust Book (em inglês): como o
vec!é definido, além de uma introdução às macros procedurais, como#[derive]. macro_rules!— Rust by Example (em inglês): exemplos curtos, que você pode rodar, de designadores, sobrecarga e repetição.- Macros By Example — The Rust Reference (em inglês): a lista completa de tipos de fragmento, como
expr,identett, e como a correspondência funciona em detalhes. - The Little Book of Rust Macros (em inglês): um mergulho gratuito e amigável no assunto, para quando você quiser escrever macros maiores.