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 como paste para concatenação de identificadores.

Referências

Exercícios

  1. Crie uma macro min! que retorna o menor valor entre dois números (use expr). Teste com min!(10, 20) e min!(7, 3).

✓ Resposta:
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
}
  • Escreva uma macro soma! que aceita uma lista de expressões separadas por vírgula e retorna a soma (use repetição).
  • ✓ Resposta:
    macro_rules! soma {
        ($($x:expr),*) => {{
            let mut total = 0;
            $( total += $x; )*
            total
        }};
    }
    
    fn main() {
        println!("{}", soma!(1, 2, 3)); // 6
        println!("{}", soma!());        // 0
    }
  • Defina uma macro 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.
  • ✓ Resposta:
    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);
    }
  • Explique o que é higiene em macros e dê um exemplo onde ela evita conflito de nomes.
  • ✓ Resposta: Higiene é a propriedade que impede que identificadores introduzidos pela macro colidam com os do contexto de chamada. Exemplo:
    macro_rules! cria_temp {
        () => { let temp = 10; temp * 2 };
    }
    
    fn main() {
        let temp = 100;
        let valor = cria_temp!();
        println!("{}", temp); // 100, não 10
    }
  • Crie uma macro 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.
  • ✓ Resposta:
    macro_rules! imprima {
        ($fmt:expr, $($arg:expr),*) => {
            println!($fmt, $($arg),*);
        };
    }
    
    fn main() {
        imprima!("{} e {}", 1, 2);
    }