Usando macros em Rust para reduzir código repetitivo
1. Introdução às Macros em Rust
Em Rust, a repetição de código é um problema comum que afeta a produtividade e a manutenibilidade do software. Macros oferecem uma solução poderosa para gerar código automaticamente em tempo de compilação, eliminando padrões repetitivos que funções genéricas ou traits não conseguem resolver adequadamente.
Macros em Rust dividem-se em duas categorias principais: macros declarativas (macro_rules!), que operam por correspondência de padrões, e macros procedurais, que manipulam a árvore sintática abstrata (AST) diretamente. Enquanto funções genéricas trabalham com tipos e traits com comportamentos, macros operam no nível sintático, permitindo transformações que seriam impossíveis apenas com genéricos.
A escolha entre macros, funções genéricas e traits depende do problema: use funções genéricas para polimorfismo paramétrico, traits para comportamentos compartilhados e macros quando precisar gerar código estruturalmente diferente ou manipular a sintaxe.
2. Macros Declarativas (macro_rules!) na Prática
A sintaxe básica de uma macro declarativa segue o padrão:
macro_rules! nome_da_macro {
($padrao:tt) => {
// código de expansão
};
}
Um exemplo prático é recriar o vec! padrão:
macro_rules! meu_vec {
($($x:expr),*) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
let v = meu_vec![1, 2, 3, 4];
Para logging condicional, podemos criar:
macro_rules! log_debug {
($($arg:tt)*) => {
if cfg!(debug_assertions) {
println!("[DEBUG] {}", format_args!($($arg)*));
}
};
}
log_debug!("Valor calculado: {}", resultado);
Higiene de macros é crucial: variáveis criadas dentro da expansão não vazam para o escopo externo, evitando conflitos de nome. Use $crate para referenciar itens do crate atual.
3. Reduzindo Boilerplate com Macros para Traits
Implementar traits manualmente para múltiplos tipos é tedioso. Macros podem automatizar isso:
macro_rules! impl_display_para_enum {
($nome:ident, $($variante:ident => $mensagem:expr),*) => {
impl std::fmt::Display for $nome {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
$(Self::$variante => write!(f, $mensagem)),*
}
}
}
};
}
enum Status {
Ativo,
Inativo,
Pendente,
}
impl_display_para_enum!(Status,
Ativo => "ativo",
Inativo => "inativo",
Pendente => "pendente"
);
Para getters/setters em structs:
macro_rules! criar_getters_setters {
($struct:ident, $($campo:ident: $tipo:ty),*) => {
impl $struct {
$(
pub fn $campo(&self) -> &$tipo {
&self.$campo
}
pub fn set_$campo(&mut self, valor: $tipo) {
self.$campo = valor;
}
)*
}
};
}
struct Pessoa {
nome: String,
idade: u32,
}
criar_getters_setters!(Pessoa, nome: String, idade: u32);
4. Macros para Padrões de Código Repetitivos
Match statements complexos podem ser simplificados:
macro_rules! match_option {
($expr:expr, $padrao:pat => $acao:expr) => {
match $expr {
Some($padrao) => $acao,
None => panic!("Esperava valor Some, mas encontrou None"),
}
};
}
let valor = Some(42);
match_option!(valor, x => println!("Valor: {}", x));
Para validação de argumentos:
macro_rules! validar_intervalo {
($valor:expr, $min:expr, $max:expr) => {
if $valor < $min || $valor > $max {
panic!("Valor {} fora do intervalo [{}, {}]", $valor, $min, $max);
}
};
}
validar_intervalo!(idade, 0, 150);
Testes unitários repetitivos também se beneficiam:
macro_rules! testar_soma {
($nome:ident, $a:expr, $b:expr, $esperado:expr) => {
#[test]
fn $nome() {
assert_eq!(soma($a, $b), $esperado);
}
};
}
testar_soma!(teste_soma_positivos, 2, 3, 5);
testar_soma!(teste_soma_negativos, -1, -2, -3);
5. Macros Procedurais: Derive e Atributos
Macros procedurais oferecem poder ilimitado para transformar código. Um exemplo de #[derive(JsonSerialize)] simplificado:
// Em um crate separado (ex: meu_derive)
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(JsonSerialize)]
pub fn json_serialize_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let nome = &input.ident;
let expanded = quote! {
impl JsonSerialize for #nome {
fn to_json(&self) -> String {
// Implementação genérica
format!("{{ \"tipo\": \"{}\" }}", stringify!(#nome))
}
}
};
TokenStream::from(expanded)
}
Macros de atributo permitem anotar funções:
// Macro de atributo #[log_call]
#[proc_macro_attribute]
pub fn log_call(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(item as ItemFn);
let nome = &input_fn.sig.ident;
let bloco = &input_fn.block;
let expanded = quote! {
fn #nome() {
println!("Chamando função: {}", stringify!(#nome));
#bloco
println!("Finalizou função: {}", stringify!(#nome));
}
};
TokenStream::from(expanded)
}
Macros procedurais são ideais para lógica complexa que requer análise da AST.
6. Boas Práticas e Armadilhas Comuns
Evite expansão excessiva: macros que geram centenas de linhas podem aumentar drasticamente o tempo de compilação. Prefira funções auxiliares quando possível.
Depuração com cargo expand:
cargo install cargo-expand
cargo expand
Use eprintln! dentro de macros para depuração temporária.
Documentação e testes: escreva testes unitários para cada macro e documente padrões de uso esperados.
7. Caso de Uso Real: Macro para DSL de Configuração
Construindo uma macro config! que gera structs e parsers:
macro_rules! config {
($nome:ident { $($campo:ident: $tipo:ty = $padrao:expr),* }) => {
struct $nome {
$($campo: $tipo),*
}
impl $nome {
fn from_json(json: &str) -> Result<Self, serde_json::Error> {
#[derive(serde::Deserialize)]
struct Temp {
$($campo: Option<$tipo>),*
}
let temp: Temp = serde_json::from_str(json)?;
Ok($nome {
$($campo: temp.$campo.unwrap_or($padrao)),*
})
}
}
};
}
config!(ConfigApp {
porta: u16 = 8080,
host: String = "localhost".to_string(),
debug: bool = false
});
Redução de linhas: sem macro, seriam ~30 linhas por struct. Com macro, apenas 5 linhas de declaração.
8. Conclusão e Próximos Passos
Macros em Rust são ferramentas essenciais para reduzir código repetitivo, aumentar a produtividade e melhorar a manutenibilidade. Desde macros declarativas simples até procedurais complexas, elas permitem abstrair padrões que seriam impossíveis com funções genéricas.
Para aprofundar, explore macro_metaprogramming, a crate proc_macro2 e pratique criando suas próprias macros. Um projeto prático sugerido: implemente uma macro #[derive(Builder)] para gerar o padrão Builder automaticamente.
Referências
- The Rust Reference - Macros — Documentação oficial sobre macros declarativas e procedurais, incluindo sintaxe e regras de higiene.
- The Little Book of Rust Macros — Guia completo e prático sobre metaprogramação com macros em Rust, com exemplos detalhados.
- Rust by Example - Macros — Exemplos interativos de macros declarativas, desde conceitos básicos até avançados.
- Proc Macro Workshop — Conjunto de exercícios práticos para aprender a criar macros procedurais com
synequote. - Serde Documentation - Custom Serialization — Guia oficial sobre como criar derives customizados para serialização, usando macros procedurais.