O Visitor Pattern (Padrão Visitante) permite adicionar novas operações a uma estrutura de objetos sem modificar as classes (ou, em Rust, os tipos) que compõem essa estrutura. Ele resolve o problema do duplo despacho (double dispatch): selecionar o comportamento com base tanto no tipo do visitante quanto no tipo do elemento visitado.
Em Rust, o Visitor Pattern tem duas implementações idiomáticas: a baseada em enum (mais comum e recomendada quando os tipos são conhecidos) e a baseada em traits (para extensibilidade). A versão com enum se beneficia do pattern matching exaustivo, enquanto a versão com traits permite que novos visitantes sejam adicionados sem recompilar a estrutura.
Problema
Considere um compilador com uma AST (Abstract Syntax Tree). A AST tem vários tipos de nós (literais, operações binárias, chamadas de função). Precisamos executar diversas operações sobre a AST — impressão, cálculo, otimização, verificação de tipos — sem modificar os nós da árvore a cada nova operação.
Sem o Visitor Pattern, cada operação exigiria modificar todos os tipos de nó, violando o Princípio Aberto/Fechado.
Solução em Rust
Abordagem 1: Visitor com Enum (Mais Idiomático)
/// Nós da AST representados como enum
#[derive(Debug, Clone)]
enum Expressao {
/// Literal numérico: 42, 3.14
Numero(f64),
/// Literal string: "olá"
Texto(String),
/// Literal booleano: true, false
Booleano(bool),
/// Operação binária: esquerda op direita
OperacaoBinaria {
esquerda: Box<Expressao>,
operador: Operador,
direita: Box<Expressao>,
},
/// Operação unária: op expressão
OperacaoUnaria {
operador: OperadorUnario,
operando: Box<Expressao>,
},
/// Chamada de função: nome(args...)
ChamadaFuncao {
nome: String,
argumentos: Vec<Expressao>,
},
/// Condicional: if cond then else
SeEntao {
condicao: Box<Expressao>,
entao: Box<Expressao>,
senao: Option<Box<Expressao>>,
},
}
#[derive(Debug, Clone, Copy)]
enum Operador {
Soma,
Subtracao,
Multiplicacao,
Divisao,
Igual,
Maior,
Menor,
}
#[derive(Debug, Clone, Copy)]
enum OperadorUnario {
Negacao, // -x
NegacaoLogica, // !x
}
/// Trait do Visitor — define uma operação sobre a AST
trait VisitanteExpressao {
type Resultado;
fn visitar(&self, expr: &Expressao) -> Self::Resultado;
}
/// Visitor 1: Impressão formatada (pretty print)
struct ImpressorFormatado {
indentacao: usize,
}
impl ImpressorFormatado {
fn novo() -> Self {
ImpressorFormatado { indentacao: 0 }
}
fn formatar_operador(&self, op: &Operador) -> &str {
match op {
Operador::Soma => "+",
Operador::Subtracao => "-",
Operador::Multiplicacao => "*",
Operador::Divisao => "/",
Operador::Igual => "==",
Operador::Maior => ">",
Operador::Menor => "<",
}
}
}
impl VisitanteExpressao for ImpressorFormatado {
type Resultado = String;
fn visitar(&self, expr: &Expressao) -> String {
match expr {
Expressao::Numero(n) => {
if *n == (*n as i64) as f64 {
format!("{}", *n as i64)
} else {
format!("{}", n)
}
}
Expressao::Texto(s) => format!("\"{}\"", s),
Expressao::Booleano(b) => format!("{}", b),
Expressao::OperacaoBinaria { esquerda, operador, direita } => {
format!(
"({} {} {})",
self.visitar(esquerda),
self.formatar_operador(operador),
self.visitar(direita),
)
}
Expressao::OperacaoUnaria { operador, operando } => {
let simbolo = match operador {
OperadorUnario::Negacao => "-",
OperadorUnario::NegacaoLogica => "!",
};
format!("{}{}", simbolo, self.visitar(operando))
}
Expressao::ChamadaFuncao { nome, argumentos } => {
let args: Vec<String> = argumentos.iter()
.map(|a| self.visitar(a))
.collect();
format!("{}({})", nome, args.join(", "))
}
Expressao::SeEntao { condicao, entao, senao } => {
let mut resultado = format!(
"if {} {{ {} }}",
self.visitar(condicao),
self.visitar(entao),
);
if let Some(s) = senao {
resultado.push_str(&format!(" else {{ {} }}", self.visitar(s)));
}
resultado
}
}
}
}
/// Visitor 2: Avaliador (calcula o resultado)
struct Avaliador;
#[derive(Debug, Clone)]
enum Valor {
Numero(f64),
Texto(String),
Booleano(bool),
Nulo,
}
impl std::fmt::Display for Valor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Valor::Numero(n) => write!(f, "{}", n),
Valor::Texto(s) => write!(f, "{}", s),
Valor::Booleano(b) => write!(f, "{}", b),
Valor::Nulo => write!(f, "nulo"),
}
}
}
impl VisitanteExpressao for Avaliador {
type Resultado = Result<Valor, String>;
fn visitar(&self, expr: &Expressao) -> Result<Valor, String> {
match expr {
Expressao::Numero(n) => Ok(Valor::Numero(*n)),
Expressao::Texto(s) => Ok(Valor::Texto(s.clone())),
Expressao::Booleano(b) => Ok(Valor::Booleano(*b)),
Expressao::OperacaoBinaria { esquerda, operador, direita } => {
let esq = self.visitar(esquerda)?;
let dir = self.visitar(direita)?;
match (esq, operador, dir) {
(Valor::Numero(a), Operador::Soma, Valor::Numero(b)) => {
Ok(Valor::Numero(a + b))
}
(Valor::Numero(a), Operador::Subtracao, Valor::Numero(b)) => {
Ok(Valor::Numero(a - b))
}
(Valor::Numero(a), Operador::Multiplicacao, Valor::Numero(b)) => {
Ok(Valor::Numero(a * b))
}
(Valor::Numero(a), Operador::Divisao, Valor::Numero(b)) => {
if b == 0.0 {
Err("Divisão por zero".to_string())
} else {
Ok(Valor::Numero(a / b))
}
}
(Valor::Numero(a), Operador::Maior, Valor::Numero(b)) => {
Ok(Valor::Booleano(a > b))
}
(Valor::Numero(a), Operador::Menor, Valor::Numero(b)) => {
Ok(Valor::Booleano(a < b))
}
(Valor::Texto(a), Operador::Soma, Valor::Texto(b)) => {
Ok(Valor::Texto(format!("{}{}", a, b)))
}
_ => Err("Operação não suportada para estes tipos".to_string()),
}
}
Expressao::OperacaoUnaria { operador, operando } => {
let val = self.visitar(operando)?;
match (operador, val) {
(OperadorUnario::Negacao, Valor::Numero(n)) => Ok(Valor::Numero(-n)),
(OperadorUnario::NegacaoLogica, Valor::Booleano(b)) => {
Ok(Valor::Booleano(!b))
}
_ => Err("Operação unária não suportada".to_string()),
}
}
Expressao::ChamadaFuncao { nome, argumentos } => {
let args: Result<Vec<Valor>, String> = argumentos.iter()
.map(|a| self.visitar(a))
.collect();
let args = args?;
match nome.as_str() {
"max" => {
let nums: Result<Vec<f64>, _> = args.iter().map(|v| {
if let Valor::Numero(n) = v { Ok(*n) }
else { Err("max() aceita apenas números".to_string()) }
}).collect();
let nums = nums?;
Ok(Valor::Numero(nums.into_iter().fold(f64::NEG_INFINITY, f64::max)))
}
"len" => {
if let Some(Valor::Texto(s)) = args.first() {
Ok(Valor::Numero(s.len() as f64))
} else {
Err("len() aceita apenas texto".to_string())
}
}
_ => Err(format!("Função '{}' não definida", nome)),
}
}
Expressao::SeEntao { condicao, entao, senao } => {
let cond = self.visitar(condicao)?;
match cond {
Valor::Booleano(true) => self.visitar(entao),
Valor::Booleano(false) => {
if let Some(s) = senao {
self.visitar(s)
} else {
Ok(Valor::Nulo)
}
}
_ => Err("Condição deve ser booleana".to_string()),
}
}
}
}
}
/// Visitor 3: Contador de nós
struct ContadorNos;
impl VisitanteExpressao for ContadorNos {
type Resultado = usize;
fn visitar(&self, expr: &Expressao) -> usize {
match expr {
Expressao::Numero(_) | Expressao::Texto(_) | Expressao::Booleano(_) => 1,
Expressao::OperacaoBinaria { esquerda, direita, .. } => {
1 + self.visitar(esquerda) + self.visitar(direita)
}
Expressao::OperacaoUnaria { operando, .. } => 1 + self.visitar(operando),
Expressao::ChamadaFuncao { argumentos, .. } => {
1 + argumentos.iter().map(|a| self.visitar(a)).sum::<usize>()
}
Expressao::SeEntao { condicao, entao, senao } => {
1 + self.visitar(condicao) + self.visitar(entao)
+ senao.as_ref().map_or(0, |s| self.visitar(s))
}
}
}
}
/// Função auxiliar para construir expressões
fn num(n: f64) -> Expressao {
Expressao::Numero(n)
}
fn binop(esq: Expressao, op: Operador, dir: Expressao) -> Expressao {
Expressao::OperacaoBinaria {
esquerda: Box::new(esq),
operador: op,
direita: Box::new(dir),
}
}
fn main() {
// Construindo AST: max(3 + 4, 2 * 5)
let ast = Expressao::ChamadaFuncao {
nome: "max".to_string(),
argumentos: vec![
binop(num(3.0), Operador::Soma, num(4.0)),
binop(num(2.0), Operador::Multiplicacao, num(5.0)),
],
};
let impressor = ImpressorFormatado::novo();
let avaliador = Avaliador;
let contador = ContadorNos;
// Mesmo AST, três operações diferentes — sem modificar a AST!
println!("Expressão: {}", impressor.visitar(&ast));
println!("Resultado: {}", avaliador.visitar(&ast).unwrap());
println!("Nós: {}", contador.visitar(&ast));
// AST mais complexa: if (10 > 5) { 10 + 20 } else { 0 }
let ast_condicional = Expressao::SeEntao {
condicao: Box::new(binop(num(10.0), Operador::Maior, num(5.0))),
entao: Box::new(binop(num(10.0), Operador::Soma, num(20.0))),
senao: Some(Box::new(num(0.0))),
};
println!("\nExpressão: {}", impressor.visitar(&ast_condicional));
println!("Resultado: {}", avaliador.visitar(&ast_condicional).unwrap());
println!("Nós: {}", contador.visitar(&ast_condicional));
}
Diagrama
Visitor Pattern — Duplo Despacho:
┌──────────────────┐ ┌───────────────────────┐
│ Expressao │ │ VisitanteExpressao │
│ (enum) │ │ (trait) │
│──────────────────│ │───────────────────────│
│ ◆ Numero │ │ + visitar(&Expressao) │
│ ◆ Texto │ │ -> Resultado │
│ ◆ Booleano │ └──────────┬────────────┘
│ ◆ OperacaoBinaria│ │
│ ◆ OperacaoUnaria │ ┌──────────┼──────────┐
│ ◆ ChamadaFuncao │ │ │ │
│ ◆ SeEntao │ ▼ ▼ ▼
└──────────────────┘ ┌────────┐ ┌────────┐ ┌────────┐
│Impressor│ │Avaliador│ │Contador│
│Formatado│ │ │ │Nos │
└────────┘ └────────┘ └────────┘
O match no enum faz o "despacho" pelo tipo do nó:
visitor.visitar(expr)
│
└──▶ match expr {
Numero(_) => ... lógica para número
Texto(_) => ... lógica para texto
OperacaoBinaria {} => ... lógica para binop
... => ... (exaustivo!)
}
Cada visitor implementa sua própria lógica para cada variante.
Novos visitors podem ser adicionados sem mudar a AST.
Exemplo do Mundo Real
Um formatador de código simplificado que visita nós de uma AST e produz código formatado com indentação:
/// Formatador de código com configuração de estilo
struct FormatadorCodigo {
largura_indentacao: usize,
usar_espacos: bool,
max_largura_linha: usize,
}
impl FormatadorCodigo {
fn novo() -> Self {
FormatadorCodigo {
largura_indentacao: 4,
usar_espacos: true,
max_largura_linha: 80,
}
}
fn indentar(&self, nivel: usize) -> String {
let char_indent = if self.usar_espacos { ' ' } else { '\t' };
let largura = if self.usar_espacos {
self.largura_indentacao * nivel
} else {
nivel
};
std::iter::repeat(char_indent).take(largura).collect()
}
fn formatar_expressao(&self, expr: &Expressao, nivel: usize) -> String {
let indent = self.indentar(nivel);
match expr {
Expressao::Numero(n) => format!("{}", n),
Expressao::Texto(s) => format!("\"{}\"", s),
Expressao::Booleano(b) => format!("{}", b),
Expressao::OperacaoBinaria { esquerda, operador, direita } => {
let esq = self.formatar_expressao(esquerda, nivel);
let dir = self.formatar_expressao(direita, nivel);
let op = match operador {
Operador::Soma => "+",
Operador::Subtracao => "-",
Operador::Multiplicacao => "*",
Operador::Divisao => "/",
Operador::Igual => "==",
Operador::Maior => ">",
Operador::Menor => "<",
};
let inline = format!("{} {} {}", esq, op, dir);
if inline.len() + indent.len() <= self.max_largura_linha {
inline
} else {
// Quebra em múltiplas linhas se exceder largura
format!(
"{}\n{}{} {}",
esq,
self.indentar(nivel + 1),
op,
dir,
)
}
}
Expressao::ChamadaFuncao { nome, argumentos } => {
let args: Vec<String> = argumentos.iter()
.map(|a| self.formatar_expressao(a, nivel + 1))
.collect();
let inline = format!("{}({})", nome, args.join(", "));
if inline.len() + indent.len() <= self.max_largura_linha {
inline
} else {
let args_formatados = args.join(&format!(",\n{}", self.indentar(nivel + 1)));
format!(
"{}(\n{}{}\n{})",
nome,
self.indentar(nivel + 1),
args_formatados,
indent,
)
}
}
Expressao::SeEntao { condicao, entao, senao } => {
let mut resultado = format!(
"if {} {{\n{}{}\n{}}}",
self.formatar_expressao(condicao, nivel),
self.indentar(nivel + 1),
self.formatar_expressao(entao, nivel + 1),
indent,
);
if let Some(s) = senao {
resultado.push_str(&format!(
" else {{\n{}{}\n{}}}",
self.indentar(nivel + 1),
self.formatar_expressao(s, nivel + 1),
indent,
));
}
resultado
}
_ => format!("{:?}", expr),
}
}
}
fn main() {
let formatador = FormatadorCodigo::novo();
let ast = Expressao::SeEntao {
condicao: Box::new(binop(num(10.0), Operador::Maior, num(5.0))),
entao: Box::new(Expressao::ChamadaFuncao {
nome: "println".to_string(),
argumentos: vec![Expressao::Texto("Maior!".to_string())],
}),
senao: Some(Box::new(Expressao::ChamadaFuncao {
nome: "println".to_string(),
argumentos: vec![Expressao::Texto("Menor ou igual".to_string())],
})),
};
println!("Código formatado:");
println!("{}", formatador.formatar_expressao(&ast, 0));
}
Quando Usar
- Múltiplas operações sobre estrutura estável: A AST raramente muda, mas novas operações são frequentes
- Separação de responsabilidades: Manter lógica de travessia separada da lógica de processamento
- Análise e transformação de árvores: Compiladores, formatadores, linters, transpiladores
- Relatórios variados: Gerar diferentes formatos (JSON, XML, texto) a partir da mesma estrutura
Quando NÃO Usar
- Estrutura muda frequentemente: Se novos tipos de nó são adicionados constantemente, todos os visitors precisam ser atualizados
- Única operação: Se há apenas uma operação, um método direto na estrutura é mais simples
- Estruturas simples: Para estruturas planas (sem recursão), iteradores são suficientes
- Performance crítica: A indireção de trait objects pode impactar performance em hot paths
Variações em Rust
Visitor mutável para transformações
/// Visitor que transforma a AST (otimizador)
trait TransformadorExpressao {
fn transformar(&self, expr: Expressao) -> Expressao;
}
/// Otimizador: avalia expressões constantes em compilação
struct OtimizadorConstantes;
impl TransformadorExpressao for OtimizadorConstantes {
fn transformar(&self, expr: Expressao) -> Expressao {
match expr {
Expressao::OperacaoBinaria { esquerda, operador, direita } => {
let esq = self.transformar(*esquerda);
let dir = self.transformar(*direita);
// Se ambos são números, calcula em compilação
if let (Expressao::Numero(a), Expressao::Numero(b)) = (&esq, &dir) {
match operador {
Operador::Soma => return Expressao::Numero(a + b),
Operador::Multiplicacao => return Expressao::Numero(a * b),
_ => {}
}
}
Expressao::OperacaoBinaria {
esquerda: Box::new(esq),
operador,
direita: Box::new(dir),
}
}
outro => outro,
}
}
}
Padrões Relacionados
- Iterator: Iteradores percorrem sequências linearmente; Visitors percorrem árvores
- Composite: Visitor é frequentemente usado com estruturas Composite (árvores)
- Strategy: Cada visitor é essencialmente uma strategy aplicada à estrutura
- Command: Visitors podem ser tratados como comandos aplicados a estruturas
Conclusão
O Visitor Pattern em Rust se beneficia enormemente de enums e pattern matching. A abordagem com enum é a mais idiomática: cada visitor implementa um match exaustivo sobre as variantes da AST, e o compilador garante que nenhum caso seja esquecido. Quando um novo tipo de nó é adicionado ao enum, todos os visitors quebram em compilação, forçando a atualização. Esta é uma vantagem significativa sobre linguagens com herança, onde um novo tipo pode silenciosamente usar uma implementação padrão incorreta. Para ASTs e estruturas similares, o Visitor com enum é a escolha clara em Rust.