Macros declarativas (macro_rules!)
Esta aula explora as macros declarativas em Rust usando macro_rules!, cobrindo sintaxe, padrões de repetição, higiene e exemplos práticos. Você aprenderá a definir e usar macros para reduzir código repetitivo, compreender a captura de tokens e a importância da higiene para evitar conflitos de nomes.
Macros declarativas, definidas com macro_rules!, são uma das formas mais poderosas de metaprogramação em Rust. Elas permitem escrever código que gera código em tempo de compilação, reduzindo repetição e permitindo abstrações que não são possíveis apenas com funções. Nesta aula, você vai aprender a sintaxe básica, como usar padrões de repetição para capturar múltiplos argumentos, e o conceito de higiene que garante que as macros não interfiram com variáveis locais.
Diferente de macros procedurais (como derive ou proc_macro), as macros declarativas são mais simples e amplamente usadas para tarefas como criar DSLs (Domain-Specific Languages) ou gerar implementações repetitivas. Vamos mergulhar nos detalhes com exemplos práticos.
Sintaxe
A sintaxe básica de uma macro declarativa é:
macro_rules! nome_da_macro {
(padrão) => {
// expansão
};
// mais regras...
}O padrão é uma sequência de tokens que a macro deve corresponder (como uma expressão, um tipo, um identificador, etc.). A expansão é o código que será gerado, que pode incluir capturas feitas no padrão. Cada regra termina com um ponto e vírgula, e a macro pode ter múltiplas regras, tentadas em ordem.
Um exemplo simples é uma macro que imprime uma mensagem com um valor:
macro_rules! imprime_valor {
($x:expr) => {
println!("Valor: {}", $x);
};
}
fn main() {
imprime_valor!(42);
}Aqui, $x:expr captura uma expressão e a usa na expansão. Os fragmentos especificadores (como expr, ty, ident, block etc.) definem o tipo de token que pode ser capturado.
Padrões e repetição
Um dos recursos mais poderosos das macros é a repetição, que permite capturar listas de argumentos. A sintaxe usa $(...) para um grupo repetido, seguido de um separador (opcional) e um operador de repetição (* para zero ou mais, + para um ou mais, ? para zero ou um).
Por exemplo, uma macro que soma uma lista de números:
macro_rules! soma {
($($x:expr),*) => {{
let mut total = 0;
$( total += $x; )*
total
}};
}
fn main() {
let resultado = soma!(1, 2, 3, 4);
println!("Soma: {}", resultado); // 10
}No padrão, $($x:expr),* captura zero ou mais expressões separadas por vírgula. Na expansão, $( total += $x; )* repete o código para cada captura. É crucial que as variáveis de repetição sejam usadas dentro do mesmo bloco de repetição na expansão.
Também é possível usar repetição com separadores e múltiplos capturadores, como em $($chave:ident => $valor:expr),* para construir um mapa.
Higiene
Higiene é uma propriedade das macros que evita conflitos de identificadores. Em Rust, macros são higiênicas por padrão: variáveis locais criadas dentro da expansão não vazam para o contexto externo, e variáveis do contexto externo não são acidentalmente capturadas pela macro.
Por exemplo, se uma macro usa uma variável temporária chamada temp, ela não colidirá com uma variável temp que exista no ponto de chamada:
macro_rules! cria_temp {
() => {
let temp = 10;
temp * 2
};
}
fn main() {
let temp = 100;
let valor = cria_temp!();
println!("temp externo: {}, valor: {}", temp, valor); // temp externo: 100, valor: 20
}Isso é possível porque o compilador renomeia internamente os identificadores introduzidos pela macro (usando um contexto de sintaxe especial). No entanto, a higiene não se aplica a itens (funções, structs, etc.) que são definidos em nível de módulo: eles são visíveis globalmente. Para evitar conflitos nesses casos, use nomes únicos ou técnicas como paste (em macros procedurais).
É importante lembrar que a higiene protege contra colisões de nomes, mas não impede que a macro use variáveis que existem no contexto de chamada se você as capturar explicitamente com $var:ident.
Exemplos
Vamos ver alguns exemplos mais elaborados de macros declarativas.
1. Macro para criar vetores
O Rust já tem a macro vec!, mas podemos criar uma versão simples:
macro_rules! meu_vec {
($($x:expr),*) => {{
let mut v = Vec::new();
$( v.push($x); )*
v
}};
}
fn main() {
let v = meu_vec![1, 2, 3];
println!("{:?}", v); // [1, 2, 3]
}2. Macro para definir getters e setters
macro_rules! impl_get_set {
($struct_name:ident, $campo:ident, $tipo:ty) => {
impl $struct_name {
pub fn get_$campo(&self) -> &$tipo {
&self.$campo
}
pub fn set_$campo(&mut self, valor: $tipo) {
self.$campo = valor;
}
}
};
}
struct Pessoa {
nome: String,
idade: u32,
}
impl_get_set!(Pessoa, nome, String);
impl_get_set!(Pessoa, idade, u32);
fn main() {
let mut p = Pessoa { nome: "Ana".to_string(), idade: 30 };
p.set_nome("Bia".to_string());
println!("Nome: {}", p.get_nome());
}Note o uso de $campo para compor nomes de métodos com get_$campo.
3. Macro com múltiplas regras
macro_rules! calcular {
($a:expr, $b:expr) => {
$a + $b
};
($a:expr) => {
$a * 2
};
}
fn main() {
println!("{}", calcular!(3, 5)); // 8
println!("{}", calcular!(3)); // 6
}As regras são tentadas na ordem, e a primeira que corresponder é usada.
Boas práticas e observações finais
- Use macros com moderação: prefira funções quando possível, pois macros são mais difíceis de depurar e testar.
- Valide as entradas da macro o máximo possível no padrão (use especificadores restritivos como
expr,ty, etc.) para evitar erros de compilação confusos. - Documente suas macros com comentários e, se possível, use nomes bem descritivos.
- Lembre-se de que macros são expandidas em tempo de compilação, então não podem ser usadas em tempo de execução.
- Para macros mais complexas, considere usar macros procedurais (com
proc_macro) ou crates comopastepara concatenação de identificadores.
Referências
- The Rust Book: Macros
- Rust Reference: Macros By Example
- Rust by Example: Macros
- Rust Reference: Metavariables
- Rust Reference: Repetition
- Rust Reference: Hygiene
Exercícios
- Crie uma macro
min!que retorna o menor valor entre dois números (useexpr). Teste commin!(10, 20)emin!(7, 3).
macro_rules! min {
($a:expr, $b:expr) => {
if $a < $b { $a } else { $b }
};
}
fn main() {
println!("{}", min!(10, 20)); // 10
println!("{}", min!(7, 3)); // 3
}soma! que aceita uma lista de expressões separadas por vírgula e retorna a soma (use repetição).macro_rules! soma {
($($x:expr),*) => {{
let mut total = 0;
$( total += $x; )*
total
}};
}
fn main() {
println!("{}", soma!(1, 2, 3)); // 6
println!("{}", soma!()); // 0
}criar_struct! que recebe um nome de struct e uma lista de campos (nome e tipo) e gera a definição da struct. Use repetição e captura de identificadores.macro_rules! criar_struct {
($nome:ident, $($campo:ident: $tipo:ty),*) => {
struct $nome {
$( $campo: $tipo, )*
}
};
}
criar_struct!(Ponto, x: f64, y: f64);
fn main() {
let p = Ponto { x: 1.0, y: 2.0 };
println!("({}, {})", p.x, p.y);
}macro_rules! cria_temp {
() => { let temp = 10; temp * 2 };
}
fn main() {
let temp = 100;
let valor = cria_temp!();
println!("{}", temp); // 100, não 10
}imprima! que aceita um formato e uma lista de argumentos (como println!) e imprime usando println!. Use repetição e capture o formato como expr.macro_rules! imprima {
($fmt:expr, $($arg:expr),*) => {
println!($fmt, $($arg),*);
};
}
fn main() {
imprima!("{} e {}", 1, 2);
}